@opengeni/db 0.28.1 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,1186 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { stableJson, type CapabilityCatalogAuthKind } from "@opengeni/contracts";
4
+ import { and, asc, eq, inArray, ne, or, sql } from "drizzle-orm";
5
+
6
+ import {
7
+ assertCapabilityComponentVersionCanChange,
8
+ cleanupOrphanedCapabilityComponents,
9
+ lockCapabilityComponentIdentity,
10
+ } from "./capability-components";
11
+ import { setSubjectRlsContext, withRlsContext, withWorkspaceRls, type Database } from "./database";
12
+ import {
13
+ removeIntegrationFeatureBindingOwner,
14
+ removeIntegrationFeatureBindingOwnersForOwner,
15
+ } from "./integration-bindings";
16
+ import * as schema from "./schema";
17
+
18
+ export type PluginBomComponent = {
19
+ key: string;
20
+ kind: "skill" | "integration" | "mcp";
21
+ capabilityId: string;
22
+ digest: string;
23
+ };
24
+
25
+ export type InstalledPluginPackage = {
26
+ pluginKey: string;
27
+ version: string;
28
+ pluginId: string;
29
+ pluginVersionId: string;
30
+ pluginInstallationId: string;
31
+ installationVersion: number;
32
+ manifest: Record<string, unknown>;
33
+ status: string;
34
+ };
35
+
36
+ export type InstalledPluginPackageSummary = {
37
+ pluginKey: string;
38
+ version: string;
39
+ name: string;
40
+ description: string;
41
+ category: string;
42
+ tags: string[];
43
+ sourceUrl: string | null;
44
+ manifestDigest: string;
45
+ installationVersion: number;
46
+ componentCount: number;
47
+ status: "active" | "needs_attention";
48
+ installedAt: string;
49
+ updatedAt: string;
50
+ };
51
+
52
+ export type PreparedPluginPackage = InstalledPluginPackage & {
53
+ operationId: string;
54
+ replayResult: Record<string, unknown> | null;
55
+ };
56
+
57
+ export class PluginOperationIdempotencyError extends Error {
58
+ readonly name = "PluginOperationIdempotencyError";
59
+ }
60
+
61
+ export class PluginInstallationVersionConflictError extends Error {
62
+ readonly name = "PluginInstallationVersionConflictError";
63
+ }
64
+
65
+ export class PluginInstallationVersionRequiredError extends Error {
66
+ readonly name = "PluginInstallationVersionRequiredError";
67
+ }
68
+
69
+ export async function getInstalledPluginPackage(
70
+ db: Database,
71
+ workspaceId: string,
72
+ pluginKey: string,
73
+ ): Promise<InstalledPluginPackage | null> {
74
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
75
+ const [row] = await scopedDb
76
+ .select({
77
+ pluginKey: schema.capabilityPlugins.pluginKey,
78
+ version: schema.capabilityPluginVersions.version,
79
+ pluginId: schema.capabilityPlugins.id,
80
+ pluginVersionId: schema.capabilityPluginVersions.id,
81
+ pluginInstallationId: schema.capabilityPluginInstallations.id,
82
+ installationVersion: schema.capabilityPluginInstallations.version,
83
+ manifest: schema.capabilityPluginVersions.manifest,
84
+ status: schema.capabilityPluginInstallations.status,
85
+ })
86
+ .from(schema.capabilityPluginInstallations)
87
+ .innerJoin(
88
+ schema.capabilityPlugins,
89
+ eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
90
+ )
91
+ .innerJoin(
92
+ schema.capabilityPluginVersions,
93
+ eq(
94
+ schema.capabilityPluginVersions.id,
95
+ schema.capabilityPluginInstallations.pluginVersionId,
96
+ ),
97
+ )
98
+ .where(
99
+ and(
100
+ eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
101
+ eq(schema.capabilityPlugins.pluginKey, pluginKey),
102
+ ),
103
+ )
104
+ .limit(1);
105
+ return row ?? null;
106
+ });
107
+ }
108
+
109
+ export async function listInstalledPluginPackages(
110
+ db: Database,
111
+ workspaceId: string,
112
+ ): Promise<InstalledPluginPackageSummary[]> {
113
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
114
+ const rows = await scopedDb
115
+ .select({
116
+ pluginKey: schema.capabilityPlugins.pluginKey,
117
+ version: schema.capabilityPluginVersions.version,
118
+ name: schema.capabilityPlugins.name,
119
+ description: schema.capabilityPlugins.description,
120
+ category: schema.capabilityPlugins.category,
121
+ tags: schema.capabilityPlugins.tags,
122
+ manifestDigest: schema.capabilityPluginVersions.manifestDigest,
123
+ manifest: schema.capabilityPluginVersions.manifest,
124
+ installationVersion: schema.capabilityPluginInstallations.version,
125
+ status: schema.capabilityPluginInstallations.status,
126
+ installedAt: schema.capabilityPluginInstallations.installedAt,
127
+ updatedAt: schema.capabilityPluginInstallations.updatedAt,
128
+ })
129
+ .from(schema.capabilityPluginInstallations)
130
+ .innerJoin(
131
+ schema.capabilityPlugins,
132
+ eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
133
+ )
134
+ .innerJoin(
135
+ schema.capabilityPluginVersions,
136
+ eq(
137
+ schema.capabilityPluginVersions.id,
138
+ schema.capabilityPluginInstallations.pluginVersionId,
139
+ ),
140
+ )
141
+ .where(
142
+ and(
143
+ eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
144
+ inArray(schema.capabilityPluginInstallations.status, ["active", "needs_attention"]),
145
+ sql`jsonb_typeof(${schema.capabilityPluginVersions.manifest} -> 'components') = 'array'`,
146
+ sql`jsonb_typeof(${schema.capabilityPluginVersions.manifest} -> 'bom') = 'array'`,
147
+ ),
148
+ )
149
+ .orderBy(asc(schema.capabilityPlugins.name), asc(schema.capabilityPlugins.pluginKey));
150
+
151
+ return rows.map((row) => {
152
+ const manifest = objectValue(row.manifest);
153
+ const sourceUrl = stringValue(manifest.sourceUrl);
154
+ const bom = pluginBom(manifest);
155
+ if (row.status !== "active" && row.status !== "needs_attention") {
156
+ throw new Error(`Unknown installed Plugin status: ${row.status}`);
157
+ }
158
+ return {
159
+ pluginKey: row.pluginKey,
160
+ version: row.version,
161
+ name: row.name,
162
+ description: row.description ?? "",
163
+ category: row.category,
164
+ tags:
165
+ stringArray(manifest.tags).length > 0
166
+ ? stringArray(manifest.tags)
167
+ : stringArray(row.tags),
168
+ sourceUrl: sourceUrl && safeHttpUrl(sourceUrl) ? sourceUrl : null,
169
+ manifestDigest: row.manifestDigest,
170
+ installationVersion: row.installationVersion,
171
+ componentCount: bom.length,
172
+ status: row.status,
173
+ installedAt: row.installedAt.toISOString(),
174
+ updatedAt: row.updatedAt.toISOString(),
175
+ };
176
+ });
177
+ });
178
+ }
179
+
180
+ export async function preparePluginPackageInstall(
181
+ db: Database,
182
+ input: {
183
+ accountId: string;
184
+ workspaceId: string;
185
+ subjectId: string;
186
+ pluginKey: string;
187
+ version: string;
188
+ name: string;
189
+ description: string;
190
+ category: string;
191
+ tags: string[];
192
+ manifestDigest: string;
193
+ manifest: Record<string, unknown>;
194
+ idempotencyKey: string;
195
+ requestDigest: string;
196
+ expectedInstallationVersion?: number;
197
+ },
198
+ ): Promise<PreparedPluginPackage> {
199
+ return await withRlsContext(
200
+ db,
201
+ { accountId: input.accountId, workspaceId: input.workspaceId },
202
+ async (scopedDb) => {
203
+ await setSubjectRlsContext(scopedDb, input.subjectId);
204
+ return await scopedDb.transaction(async (txRaw) => {
205
+ const tx = txRaw as unknown as Database;
206
+ await tx.execute(
207
+ sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
208
+ );
209
+ await tx.execute(
210
+ sql`select pg_advisory_xact_lock(hashtextextended(${`plugin-package:${input.workspaceId}:${input.pluginKey}`}, 0))`,
211
+ );
212
+ let [operation] = await tx
213
+ .select()
214
+ .from(schema.capabilityOperations)
215
+ .where(
216
+ and(
217
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
218
+ eq(schema.capabilityOperations.idempotencyKey, input.idempotencyKey),
219
+ ),
220
+ )
221
+ .for("update")
222
+ .limit(1);
223
+ const resuming = Boolean(operation);
224
+ if (operation && operation.requestDigest !== input.requestDigest) {
225
+ throw new PluginOperationIdempotencyError("Plugin idempotency key was reused");
226
+ }
227
+ if (operation?.status === "completed" && operation.result) {
228
+ const current = await pluginPackageInScope(tx, input.workspaceId, input.pluginKey);
229
+ if (!current) throw new Error("Completed Plugin operation lost its installation");
230
+ return { ...current, operationId: operation.id, replayResult: operation.result };
231
+ }
232
+ if (!operation) {
233
+ [operation] = await tx
234
+ .insert(schema.capabilityOperations)
235
+ .values({
236
+ accountId: input.accountId,
237
+ workspaceId: input.workspaceId,
238
+ idempotencyKey: input.idempotencyKey,
239
+ requestDigest: input.requestDigest,
240
+ kind: input.expectedInstallationVersion ? "update" : "install",
241
+ targetKind: "plugin",
242
+ targetId: input.pluginKey,
243
+ status: "running",
244
+ phase: "admitted",
245
+ createdBySubjectId: input.subjectId,
246
+ })
247
+ .returning();
248
+ } else {
249
+ [operation] = await tx
250
+ .update(schema.capabilityOperations)
251
+ .set({
252
+ status: "running",
253
+ phase: "resuming",
254
+ errorCode: null,
255
+ version: operation.version + 1,
256
+ updatedAt: new Date(),
257
+ })
258
+ .where(eq(schema.capabilityOperations.id, operation.id))
259
+ .returning();
260
+ }
261
+ if (!operation) throw new Error("Failed to admit Plugin operation");
262
+
263
+ let [plugin] = await tx
264
+ .select()
265
+ .from(schema.capabilityPlugins)
266
+ .where(
267
+ and(
268
+ eq(schema.capabilityPlugins.workspaceId, input.workspaceId),
269
+ eq(schema.capabilityPlugins.pluginKey, input.pluginKey),
270
+ ),
271
+ )
272
+ .for("update")
273
+ .limit(1);
274
+ if (!plugin) {
275
+ [plugin] = await tx
276
+ .insert(schema.capabilityPlugins)
277
+ .values({
278
+ pluginKey: input.pluginKey,
279
+ accountId: input.accountId,
280
+ workspaceId: input.workspaceId,
281
+ name: input.name,
282
+ description: input.description,
283
+ category: input.category,
284
+ tags: input.tags,
285
+ provenance: "workspace",
286
+ })
287
+ .returning();
288
+ } else {
289
+ [plugin] = await tx
290
+ .update(schema.capabilityPlugins)
291
+ .set({
292
+ name: input.name,
293
+ description: input.description,
294
+ category: input.category,
295
+ tags: input.tags,
296
+ updatedAt: new Date(),
297
+ })
298
+ .where(eq(schema.capabilityPlugins.id, plugin.id))
299
+ .returning();
300
+ }
301
+ if (!plugin) throw new Error("Failed to persist Plugin definition");
302
+
303
+ let [pluginVersion] = await tx
304
+ .select()
305
+ .from(schema.capabilityPluginVersions)
306
+ .where(
307
+ and(
308
+ eq(schema.capabilityPluginVersions.pluginId, plugin.id),
309
+ eq(schema.capabilityPluginVersions.version, input.version),
310
+ ),
311
+ )
312
+ .for("update")
313
+ .limit(1);
314
+ if (pluginVersion && pluginVersion.manifestDigest !== input.manifestDigest) {
315
+ throw new Error("Plugin version conflicts with immutable manifest content");
316
+ }
317
+ if (!pluginVersion) {
318
+ [pluginVersion] = await tx
319
+ .insert(schema.capabilityPluginVersions)
320
+ .values({
321
+ pluginId: plugin.id,
322
+ version: input.version,
323
+ manifestDigest: input.manifestDigest,
324
+ manifest: input.manifest,
325
+ status: "published",
326
+ })
327
+ .returning();
328
+ }
329
+ if (!pluginVersion) throw new Error("Failed to persist Plugin version");
330
+
331
+ let [installation] = await tx
332
+ .select()
333
+ .from(schema.capabilityPluginInstallations)
334
+ .where(
335
+ and(
336
+ eq(schema.capabilityPluginInstallations.workspaceId, input.workspaceId),
337
+ eq(schema.capabilityPluginInstallations.pluginId, plugin.id),
338
+ ),
339
+ )
340
+ .for("update")
341
+ .limit(1);
342
+ if (
343
+ installation &&
344
+ !resuming &&
345
+ input.expectedInstallationVersion !== undefined &&
346
+ installation.version !== input.expectedInstallationVersion
347
+ ) {
348
+ throw new PluginInstallationVersionConflictError("Plugin installation changed");
349
+ }
350
+ if (installation && !resuming && input.expectedInstallationVersion === undefined) {
351
+ throw new PluginInstallationVersionRequiredError(
352
+ "Updating a Plugin requires the previewed installation version",
353
+ );
354
+ }
355
+ if (!installation && !resuming && input.expectedInstallationVersion !== undefined) {
356
+ throw new PluginInstallationVersionConflictError("Plugin installation changed");
357
+ }
358
+ if (!installation) {
359
+ [installation] = await tx
360
+ .insert(schema.capabilityPluginInstallations)
361
+ .values({
362
+ accountId: input.accountId,
363
+ workspaceId: input.workspaceId,
364
+ pluginId: plugin.id,
365
+ pluginVersionId: pluginVersion.id,
366
+ status: "needs_attention",
367
+ installedBySubjectId: input.subjectId,
368
+ })
369
+ .returning();
370
+ } else if (!resuming) {
371
+ [installation] = await tx
372
+ .update(schema.capabilityPluginInstallations)
373
+ .set({
374
+ pluginVersionId: pluginVersion.id,
375
+ status: "needs_attention",
376
+ version: installation.version + 1,
377
+ installedBySubjectId: input.subjectId,
378
+ updatedAt: new Date(),
379
+ })
380
+ .where(eq(schema.capabilityPluginInstallations.id, installation.id))
381
+ .returning();
382
+ }
383
+ if (!installation) throw new Error("Failed to persist Plugin installation");
384
+ return {
385
+ pluginKey: input.pluginKey,
386
+ version: input.version,
387
+ pluginId: plugin.id,
388
+ pluginVersionId: pluginVersion.id,
389
+ pluginInstallationId: installation.id,
390
+ installationVersion: installation.version,
391
+ manifest: input.manifest,
392
+ status: installation.status,
393
+ operationId: operation.id,
394
+ replayResult: operation.result ?? null,
395
+ };
396
+ });
397
+ },
398
+ );
399
+ }
400
+
401
+ export async function checkpointPluginPackageOperation(
402
+ db: Database,
403
+ input: {
404
+ workspaceId: string;
405
+ operationId: string;
406
+ phase: string;
407
+ completedKeys: string[];
408
+ },
409
+ ): Promise<void> {
410
+ await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
411
+ await scopedDb
412
+ .update(schema.capabilityOperations)
413
+ .set({
414
+ status: "running",
415
+ phase: input.phase,
416
+ result: { completedKeys: input.completedKeys },
417
+ errorCode: null,
418
+ version: sql`${schema.capabilityOperations.version} + 1`,
419
+ updatedAt: new Date(),
420
+ })
421
+ .where(eq(schema.capabilityOperations.id, input.operationId));
422
+ });
423
+ }
424
+
425
+ export async function deferPluginPackageOperation(
426
+ db: Database,
427
+ input: { workspaceId: string; operationId: string; phase: string; errorCode: string },
428
+ ): Promise<void> {
429
+ await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
430
+ await scopedDb
431
+ .update(schema.capabilityOperations)
432
+ .set({
433
+ status: "pending",
434
+ phase: input.phase,
435
+ errorCode: input.errorCode.slice(0, 120),
436
+ version: sql`${schema.capabilityOperations.version} + 1`,
437
+ updatedAt: new Date(),
438
+ })
439
+ .where(eq(schema.capabilityOperations.id, input.operationId));
440
+ });
441
+ }
442
+
443
+ export async function finalizePluginPackageInstall(
444
+ db: Database,
445
+ input: {
446
+ accountId: string;
447
+ workspaceId: string;
448
+ subjectId: string;
449
+ operationId: string;
450
+ pluginInstallationId: string;
451
+ retainedFacetInstallationIds: string[];
452
+ retainedBindingIds: string[];
453
+ result: Record<string, unknown>;
454
+ },
455
+ ): Promise<void> {
456
+ await withRlsContext(
457
+ db,
458
+ { accountId: input.accountId, workspaceId: input.workspaceId },
459
+ async (scopedDb) => {
460
+ await setSubjectRlsContext(scopedDb, input.subjectId);
461
+ return await scopedDb.transaction(async (txRaw) => {
462
+ const tx = txRaw as unknown as Database;
463
+ const ownedRows = await tx
464
+ .select({
465
+ ownerId: schema.capabilityComponentOwners.id,
466
+ facetInstallationId: schema.capabilityComponentOwners.facetInstallationId,
467
+ })
468
+ .from(schema.capabilityComponentOwners)
469
+ .where(
470
+ and(
471
+ eq(schema.capabilityComponentOwners.workspaceId, input.workspaceId),
472
+ eq(schema.capabilityComponentOwners.ownerKind, "plugin"),
473
+ eq(schema.capabilityComponentOwners.ownerId, input.pluginInstallationId),
474
+ ),
475
+ );
476
+ const retained = new Set(input.retainedFacetInstallationIds);
477
+ const stale = ownedRows.filter((row) => !retained.has(row.facetInstallationId));
478
+ if (stale.length > 0) {
479
+ await tx.delete(schema.capabilityComponentOwners).where(
480
+ inArray(
481
+ schema.capabilityComponentOwners.id,
482
+ stale.map((row) => row.ownerId),
483
+ ),
484
+ );
485
+ await cleanupOrphanedCapabilityComponents(
486
+ tx,
487
+ input.workspaceId,
488
+ stale.map((row) => row.facetInstallationId),
489
+ );
490
+ }
491
+ const ownedBindings = await tx
492
+ .select({ bindingId: schema.integrationFeatureBindingOwners.bindingId })
493
+ .from(schema.integrationFeatureBindingOwners)
494
+ .where(
495
+ and(
496
+ eq(schema.integrationFeatureBindingOwners.workspaceId, input.workspaceId),
497
+ eq(schema.integrationFeatureBindingOwners.ownerKind, "plugin"),
498
+ eq(schema.integrationFeatureBindingOwners.ownerId, input.pluginInstallationId),
499
+ ),
500
+ );
501
+ const retainedBindings = new Set(input.retainedBindingIds);
502
+ for (const bindingId of [
503
+ ...new Set(
504
+ ownedBindings
505
+ .map((row) => row.bindingId)
506
+ .filter((candidateId) => !retainedBindings.has(candidateId)),
507
+ ),
508
+ ]) {
509
+ await removeIntegrationFeatureBindingOwner(tx, {
510
+ workspaceId: input.workspaceId,
511
+ bindingId,
512
+ owner: { kind: "plugin", id: input.pluginInstallationId },
513
+ });
514
+ }
515
+ await tx
516
+ .update(schema.capabilityPluginInstallations)
517
+ .set({ status: "active", updatedAt: new Date() })
518
+ .where(eq(schema.capabilityPluginInstallations.id, input.pluginInstallationId));
519
+ await tx
520
+ .update(schema.capabilityOperations)
521
+ .set({
522
+ status: "completed",
523
+ phase: "completed",
524
+ result: input.result,
525
+ errorCode: null,
526
+ version: sql`${schema.capabilityOperations.version} + 1`,
527
+ completedAt: new Date(),
528
+ updatedAt: new Date(),
529
+ })
530
+ .where(eq(schema.capabilityOperations.id, input.operationId));
531
+ });
532
+ },
533
+ );
534
+ }
535
+
536
+ export async function installPluginMcpReference(
537
+ db: Database,
538
+ input: {
539
+ accountId: string;
540
+ workspaceId: string;
541
+ subjectId: string;
542
+ ownerPluginInstallationId: string;
543
+ serverId: string;
544
+ name: string;
545
+ url: string;
546
+ allowedTools: string[];
547
+ timeoutMs?: number;
548
+ cacheToolsList?: boolean;
549
+ requireApproval?: boolean | string[];
550
+ connectionRef?: Record<string, unknown>;
551
+ authKind: CapabilityCatalogAuthKind;
552
+ digest: string;
553
+ },
554
+ ): Promise<{ capabilityId: string; facetInstallationId: string }> {
555
+ const capabilityId = `mcp:configured:${input.serverId}`;
556
+ const pluginKey = `plugin-mcp/${input.serverId}`;
557
+ return await withRlsContext(
558
+ db,
559
+ { accountId: input.accountId, workspaceId: input.workspaceId },
560
+ async (scopedDb) => {
561
+ await setSubjectRlsContext(scopedDb, input.subjectId);
562
+ return await scopedDb.transaction(async (txRaw) => {
563
+ const tx = txRaw as unknown as Database;
564
+ const now = new Date();
565
+ await lockCapabilityComponentIdentity(tx, input.workspaceId, pluginKey);
566
+ let [plugin] = await tx
567
+ .select()
568
+ .from(schema.capabilityPlugins)
569
+ .where(
570
+ and(
571
+ eq(schema.capabilityPlugins.workspaceId, input.workspaceId),
572
+ eq(schema.capabilityPlugins.pluginKey, pluginKey),
573
+ ),
574
+ )
575
+ .for("update")
576
+ .limit(1);
577
+ if (!plugin) {
578
+ [plugin] = await tx
579
+ .insert(schema.capabilityPlugins)
580
+ .values({
581
+ pluginKey,
582
+ accountId: input.accountId,
583
+ workspaceId: input.workspaceId,
584
+ name: input.name,
585
+ description: `Configured MCP reference ${input.serverId}`,
586
+ category: "integrations",
587
+ tags: ["mcp", "configured", "plugin"],
588
+ provenance: "deployment",
589
+ })
590
+ .returning();
591
+ }
592
+ if (!plugin) throw new Error("Failed to persist Plugin MCP definition");
593
+ const manifest = {
594
+ schemaVersion: 1,
595
+ kind: "mcp",
596
+ capabilityId,
597
+ serverId: input.serverId,
598
+ url: input.url,
599
+ digest: input.digest,
600
+ };
601
+ let [version] = await tx
602
+ .select()
603
+ .from(schema.capabilityPluginVersions)
604
+ .where(
605
+ and(
606
+ eq(schema.capabilityPluginVersions.pluginId, plugin.id),
607
+ eq(schema.capabilityPluginVersions.version, input.digest),
608
+ ),
609
+ )
610
+ .limit(1);
611
+ if (!version) {
612
+ [version] = await tx
613
+ .insert(schema.capabilityPluginVersions)
614
+ .values({
615
+ pluginId: plugin.id,
616
+ version: input.digest,
617
+ manifestDigest: input.digest,
618
+ manifest,
619
+ })
620
+ .returning();
621
+ }
622
+ if (!version) throw new Error("Failed to persist Plugin MCP version");
623
+ let [facet] = await tx
624
+ .select()
625
+ .from(schema.capabilityFacets)
626
+ .where(
627
+ and(
628
+ eq(schema.capabilityFacets.pluginVersionId, version.id),
629
+ eq(schema.capabilityFacets.facetKey, "mcp"),
630
+ ),
631
+ )
632
+ .limit(1);
633
+ if (!facet) {
634
+ [facet] = await tx
635
+ .insert(schema.capabilityFacets)
636
+ .values({
637
+ pluginVersionId: version.id,
638
+ facetKey: "mcp",
639
+ kind: "mcp",
640
+ activationMode: "workspace_managed",
641
+ required: true,
642
+ })
643
+ .returning();
644
+ await tx.insert(schema.capabilityMcpFacets).values({
645
+ facetId: facet!.id,
646
+ serverId: input.serverId,
647
+ endpointUrl: input.url,
648
+ transport: "streamable_http",
649
+ authKind: input.authKind,
650
+ allowedTools: input.allowedTools,
651
+ });
652
+ }
653
+ if (!facet) throw new Error("Failed to persist Plugin MCP facet");
654
+ let [installation] = await tx
655
+ .select()
656
+ .from(schema.capabilityPluginInstallations)
657
+ .where(
658
+ and(
659
+ eq(schema.capabilityPluginInstallations.workspaceId, input.workspaceId),
660
+ eq(schema.capabilityPluginInstallations.pluginId, plugin.id),
661
+ ),
662
+ )
663
+ .for("update")
664
+ .limit(1);
665
+ if (installation && installation.pluginVersionId !== version.id) {
666
+ await assertCapabilityComponentVersionCanChange(tx, {
667
+ workspaceId: input.workspaceId,
668
+ pluginKey,
669
+ pluginInstallationId: installation.id,
670
+ owner: { kind: "plugin", id: input.ownerPluginInstallationId },
671
+ });
672
+ }
673
+ if (!installation) {
674
+ [installation] = await tx
675
+ .insert(schema.capabilityPluginInstallations)
676
+ .values({
677
+ accountId: input.accountId,
678
+ workspaceId: input.workspaceId,
679
+ pluginId: plugin.id,
680
+ pluginVersionId: version.id,
681
+ status: "active",
682
+ installedBySubjectId: input.subjectId,
683
+ })
684
+ .returning();
685
+ } else if (
686
+ installation.pluginVersionId !== version.id ||
687
+ installation.status !== "active"
688
+ ) {
689
+ await tx
690
+ .delete(schema.capabilityFacetInstallations)
691
+ .where(eq(schema.capabilityFacetInstallations.pluginInstallationId, installation.id));
692
+ [installation] = await tx
693
+ .update(schema.capabilityPluginInstallations)
694
+ .set({
695
+ pluginVersionId: version.id,
696
+ status: "active",
697
+ version: installation.version + 1,
698
+ installedBySubjectId: input.subjectId,
699
+ installedAt: now,
700
+ updatedAt: now,
701
+ })
702
+ .where(eq(schema.capabilityPluginInstallations.id, installation.id))
703
+ .returning();
704
+ }
705
+ if (!installation) throw new Error("Failed to persist Plugin MCP installation");
706
+ let [facetInstallation] = await tx
707
+ .select()
708
+ .from(schema.capabilityFacetInstallations)
709
+ .where(
710
+ and(
711
+ eq(schema.capabilityFacetInstallations.pluginInstallationId, installation.id),
712
+ eq(schema.capabilityFacetInstallations.facetId, facet.id),
713
+ ),
714
+ )
715
+ .limit(1);
716
+ if (!facetInstallation) {
717
+ [facetInstallation] = await tx
718
+ .insert(schema.capabilityFacetInstallations)
719
+ .values({
720
+ accountId: input.accountId,
721
+ workspaceId: input.workspaceId,
722
+ pluginInstallationId: installation.id,
723
+ facetId: facet.id,
724
+ status: "active",
725
+ config: { serverId: input.serverId },
726
+ })
727
+ .returning();
728
+ }
729
+ if (!facetInstallation) throw new Error("Failed to persist Plugin MCP facet installation");
730
+ await tx
731
+ .insert(schema.capabilityComponentOwners)
732
+ .values({
733
+ accountId: input.accountId,
734
+ workspaceId: input.workspaceId,
735
+ facetInstallationId: facetInstallation.id,
736
+ ownerKind: "plugin",
737
+ ownerId: input.ownerPluginInstallationId,
738
+ removable: true,
739
+ })
740
+ .onConflictDoNothing();
741
+ const metadata = {
742
+ platformVersion: 2,
743
+ serverId: input.serverId,
744
+ mcpServerId: input.serverId,
745
+ pluginInstallationId: installation.id,
746
+ facetInstallationId: facetInstallation.id,
747
+ pluginOwnerInstallationId: input.ownerPluginInstallationId,
748
+ provenance: "plugin_configured_mcp",
749
+ };
750
+ await tx
751
+ .insert(schema.capabilityCatalogItems)
752
+ .values({
753
+ id: capabilityId,
754
+ accountId: input.accountId,
755
+ workspaceId: input.workspaceId,
756
+ kind: "mcp",
757
+ source: "manual",
758
+ name: input.name,
759
+ description: `Configured MCP reference ${input.serverId}`,
760
+ category: "integrations",
761
+ tags: ["mcp", "plugin"],
762
+ endpointUrl: input.url,
763
+ surfaceType: "mcp",
764
+ transport: "streamable-http",
765
+ authKind: input.authKind,
766
+ provenance: "plugin_configured_mcp",
767
+ metadata,
768
+ updatedAt: now,
769
+ })
770
+ .onConflictDoUpdate({
771
+ target: [schema.capabilityCatalogItems.workspaceId, schema.capabilityCatalogItems.id],
772
+ set: { name: input.name, endpointUrl: input.url, metadata, updatedAt: now },
773
+ });
774
+ await tx
775
+ .insert(schema.capabilityInstallations)
776
+ .values({
777
+ accountId: input.accountId,
778
+ workspaceId: input.workspaceId,
779
+ capabilityId,
780
+ kind: "mcp",
781
+ status: "active",
782
+ config: {
783
+ allowedTools: input.allowedTools,
784
+ ...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
785
+ ...(input.cacheToolsList !== undefined
786
+ ? { cacheToolsList: input.cacheToolsList }
787
+ : {}),
788
+ ...(input.requireApproval !== undefined
789
+ ? { requireApproval: input.requireApproval }
790
+ : {}),
791
+ ...(input.connectionRef ? { connectionRef: input.connectionRef } : {}),
792
+ },
793
+ metadata: {
794
+ ...metadata,
795
+ mcpConnectivity: {
796
+ status: input.connectionRef ? "auth_deferred" : "ok",
797
+ checkedAt: now.toISOString(),
798
+ source: "deployment_config",
799
+ },
800
+ },
801
+ enabledAt: now,
802
+ updatedAt: now,
803
+ })
804
+ .onConflictDoUpdate({
805
+ target: [
806
+ schema.capabilityInstallations.workspaceId,
807
+ schema.capabilityInstallations.capabilityId,
808
+ ],
809
+ set: {
810
+ status: "active",
811
+ config: sql`excluded.config`,
812
+ metadata: sql`excluded.metadata`,
813
+ updatedAt: now,
814
+ },
815
+ });
816
+ return { capabilityId, facetInstallationId: facetInstallation.id };
817
+ });
818
+ },
819
+ );
820
+ }
821
+
822
+ export async function getPluginPackageUninstallPreview(
823
+ db: Database,
824
+ workspaceId: string,
825
+ pluginKey: string,
826
+ ): Promise<{
827
+ installed: boolean;
828
+ version: string | null;
829
+ installationVersion: number | null;
830
+ components: Array<{
831
+ capabilityId: string;
832
+ kind: "skill" | "integration" | "mcp";
833
+ retainedByOtherOwners: boolean;
834
+ }>;
835
+ }> {
836
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
837
+ const plugin = await pluginPackageInScope(scopedDb, workspaceId, pluginKey);
838
+ if (!plugin || plugin.status === "disabled") {
839
+ return { installed: false, version: null, installationVersion: null, components: [] };
840
+ }
841
+ const components = await pluginOwnedComponents(
842
+ scopedDb,
843
+ workspaceId,
844
+ plugin.pluginInstallationId,
845
+ );
846
+ return {
847
+ installed: true,
848
+ version: plugin.version,
849
+ installationVersion: plugin.installationVersion,
850
+ components,
851
+ };
852
+ });
853
+ }
854
+
855
+ export async function uninstallPluginPackage(
856
+ db: Database,
857
+ input: {
858
+ accountId: string;
859
+ workspaceId: string;
860
+ subjectId: string;
861
+ pluginKey: string;
862
+ expectedInstallationVersion: number;
863
+ idempotencyKey: string;
864
+ },
865
+ ): Promise<{ status: "not_installed" | "uninstalled"; retainedComponents: string[] }> {
866
+ const requestDigest = sha256(
867
+ stableJson({
868
+ pluginKey: input.pluginKey,
869
+ expectedInstallationVersion: input.expectedInstallationVersion,
870
+ }),
871
+ );
872
+ return await withRlsContext(
873
+ db,
874
+ { accountId: input.accountId, workspaceId: input.workspaceId },
875
+ async (scopedDb) => {
876
+ await setSubjectRlsContext(scopedDb, input.subjectId);
877
+ return await scopedDb.transaction(async (txRaw) => {
878
+ const tx = txRaw as unknown as Database;
879
+ await tx.execute(
880
+ sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
881
+ );
882
+ await tx.execute(
883
+ sql`select pg_advisory_xact_lock(hashtextextended(${`plugin-package:${input.workspaceId}:${input.pluginKey}`}, 0))`,
884
+ );
885
+ const [existingOperation] = await tx
886
+ .select()
887
+ .from(schema.capabilityOperations)
888
+ .where(
889
+ and(
890
+ eq(schema.capabilityOperations.workspaceId, input.workspaceId),
891
+ eq(schema.capabilityOperations.idempotencyKey, input.idempotencyKey),
892
+ ),
893
+ )
894
+ .for("update")
895
+ .limit(1);
896
+ if (existingOperation) {
897
+ if (existingOperation.requestDigest !== requestDigest) {
898
+ throw new PluginOperationIdempotencyError("Plugin idempotency key was reused");
899
+ }
900
+ if (existingOperation.status === "completed" && existingOperation.result) {
901
+ return {
902
+ status:
903
+ existingOperation.result.status === "not_installed"
904
+ ? "not_installed"
905
+ : "uninstalled",
906
+ retainedComponents: stringArray(existingOperation.result.retainedComponents),
907
+ };
908
+ }
909
+ }
910
+ const plugin = await pluginPackageInScope(tx, input.workspaceId, input.pluginKey, true);
911
+ if (!plugin || plugin.status === "disabled") {
912
+ await completeInlineOperation(tx, input, requestDigest, {
913
+ status: "not_installed",
914
+ retainedComponents: [],
915
+ });
916
+ return { status: "not_installed", retainedComponents: [] };
917
+ }
918
+ if (plugin.installationVersion !== input.expectedInstallationVersion) {
919
+ throw new PluginInstallationVersionConflictError("Plugin installation changed");
920
+ }
921
+ const components = await pluginOwnedComponents(
922
+ tx,
923
+ input.workspaceId,
924
+ plugin.pluginInstallationId,
925
+ );
926
+ await removeIntegrationFeatureBindingOwnersForOwner(tx, {
927
+ workspaceId: input.workspaceId,
928
+ owner: { kind: "plugin", id: plugin.pluginInstallationId },
929
+ });
930
+ const owned = await tx
931
+ .delete(schema.capabilityComponentOwners)
932
+ .where(
933
+ and(
934
+ eq(schema.capabilityComponentOwners.workspaceId, input.workspaceId),
935
+ eq(schema.capabilityComponentOwners.ownerKind, "plugin"),
936
+ eq(schema.capabilityComponentOwners.ownerId, plugin.pluginInstallationId),
937
+ ),
938
+ )
939
+ .returning({ facetInstallationId: schema.capabilityComponentOwners.facetInstallationId });
940
+ await cleanupOrphanedCapabilityComponents(
941
+ tx,
942
+ input.workspaceId,
943
+ owned.map((row) => row.facetInstallationId),
944
+ );
945
+ await tx
946
+ .update(schema.capabilityPluginInstallations)
947
+ .set({
948
+ status: "disabled",
949
+ version: plugin.installationVersion + 1,
950
+ updatedAt: new Date(),
951
+ })
952
+ .where(eq(schema.capabilityPluginInstallations.id, plugin.pluginInstallationId));
953
+ const retainedComponents = components
954
+ .filter((component) => component.retainedByOtherOwners)
955
+ .map((component) => component.capabilityId);
956
+ await completeInlineOperation(tx, input, requestDigest, {
957
+ status: "uninstalled",
958
+ retainedComponents,
959
+ });
960
+ return { status: "uninstalled", retainedComponents };
961
+ });
962
+ },
963
+ );
964
+ }
965
+
966
+ async function pluginPackageInScope(
967
+ db: Database,
968
+ workspaceId: string,
969
+ pluginKey: string,
970
+ lock = false,
971
+ ): Promise<InstalledPluginPackage | null> {
972
+ let query = db
973
+ .select({
974
+ pluginKey: schema.capabilityPlugins.pluginKey,
975
+ version: schema.capabilityPluginVersions.version,
976
+ pluginId: schema.capabilityPlugins.id,
977
+ pluginVersionId: schema.capabilityPluginVersions.id,
978
+ pluginInstallationId: schema.capabilityPluginInstallations.id,
979
+ installationVersion: schema.capabilityPluginInstallations.version,
980
+ manifest: schema.capabilityPluginVersions.manifest,
981
+ status: schema.capabilityPluginInstallations.status,
982
+ })
983
+ .from(schema.capabilityPluginInstallations)
984
+ .innerJoin(
985
+ schema.capabilityPlugins,
986
+ eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
987
+ )
988
+ .innerJoin(
989
+ schema.capabilityPluginVersions,
990
+ eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
991
+ )
992
+ .where(
993
+ and(
994
+ eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
995
+ eq(schema.capabilityPlugins.pluginKey, pluginKey),
996
+ ),
997
+ )
998
+ .limit(1);
999
+ if (lock) query = query.for("update") as typeof query;
1000
+ const [row] = await query;
1001
+ return row ?? null;
1002
+ }
1003
+
1004
+ async function pluginOwnedComponents(
1005
+ db: Database,
1006
+ workspaceId: string,
1007
+ ownerPluginInstallationId: string,
1008
+ ): Promise<
1009
+ Array<{
1010
+ capabilityId: string;
1011
+ kind: "skill" | "integration" | "mcp";
1012
+ retainedByOtherOwners: boolean;
1013
+ }>
1014
+ > {
1015
+ const rows = await db
1016
+ .select({
1017
+ facetInstallationId: schema.capabilityFacetInstallations.id,
1018
+ facetKind: schema.capabilityFacets.kind,
1019
+ skillCapabilityId: schema.capabilitySkillFacets.capabilityId,
1020
+ manifest: schema.capabilityPluginVersions.manifest,
1021
+ })
1022
+ .from(schema.capabilityComponentOwners)
1023
+ .innerJoin(
1024
+ schema.capabilityFacetInstallations,
1025
+ eq(
1026
+ schema.capabilityFacetInstallations.id,
1027
+ schema.capabilityComponentOwners.facetInstallationId,
1028
+ ),
1029
+ )
1030
+ .innerJoin(
1031
+ schema.capabilityFacets,
1032
+ eq(schema.capabilityFacets.id, schema.capabilityFacetInstallations.facetId),
1033
+ )
1034
+ .innerJoin(
1035
+ schema.capabilityPluginInstallations,
1036
+ eq(
1037
+ schema.capabilityPluginInstallations.id,
1038
+ schema.capabilityFacetInstallations.pluginInstallationId,
1039
+ ),
1040
+ )
1041
+ .innerJoin(
1042
+ schema.capabilityPluginVersions,
1043
+ eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
1044
+ )
1045
+ .leftJoin(
1046
+ schema.capabilitySkillFacets,
1047
+ eq(schema.capabilitySkillFacets.facetId, schema.capabilityFacets.id),
1048
+ )
1049
+ .where(
1050
+ and(
1051
+ eq(schema.capabilityComponentOwners.workspaceId, workspaceId),
1052
+ eq(schema.capabilityComponentOwners.ownerKind, "plugin"),
1053
+ eq(schema.capabilityComponentOwners.ownerId, ownerPluginInstallationId),
1054
+ ),
1055
+ )
1056
+ .orderBy(asc(schema.capabilityFacets.kind), asc(schema.capabilityFacets.facetKey));
1057
+ const components = new Map<
1058
+ string,
1059
+ { capabilityId: string; kind: "skill" | "integration" | "mcp"; facetIds: string[] }
1060
+ >();
1061
+ for (const row of rows) {
1062
+ const manifest = objectValue(row.manifest);
1063
+ const capabilityId = row.skillCapabilityId ?? stringValue(manifest.capabilityId);
1064
+ const kind =
1065
+ row.facetKind === "skill"
1066
+ ? "skill"
1067
+ : row.facetKind === "mcp"
1068
+ ? "mcp"
1069
+ : row.facetKind === "api" || row.facetKind === "integration"
1070
+ ? "integration"
1071
+ : null;
1072
+ if (!capabilityId || !kind) continue;
1073
+ const existing = components.get(capabilityId);
1074
+ if (existing) existing.facetIds.push(row.facetInstallationId);
1075
+ else components.set(capabilityId, { capabilityId, kind, facetIds: [row.facetInstallationId] });
1076
+ }
1077
+ const result = [];
1078
+ for (const component of components.values()) {
1079
+ const [ownerCount] = await db
1080
+ .select({ count: sql<number>`count(*)::int` })
1081
+ .from(schema.capabilityComponentOwners)
1082
+ .where(
1083
+ and(
1084
+ inArray(schema.capabilityComponentOwners.facetInstallationId, component.facetIds),
1085
+ // Retention is deliberately physical, not runtime-effective. A Plugin
1086
+ // in needs_attention may be resumable and must keep shared component
1087
+ // storage even though its tools stay hidden until the operation
1088
+ // completes. Runtime projections continue to apply the effective-owner
1089
+ // predicate at their own read boundaries.
1090
+ or(
1091
+ ne(schema.capabilityComponentOwners.ownerKind, "plugin"),
1092
+ ne(schema.capabilityComponentOwners.ownerId, ownerPluginInstallationId),
1093
+ ),
1094
+ ),
1095
+ );
1096
+ result.push({
1097
+ capabilityId: component.capabilityId,
1098
+ kind: component.kind,
1099
+ retainedByOtherOwners: (ownerCount?.count ?? 0) > 0,
1100
+ });
1101
+ }
1102
+ return result;
1103
+ }
1104
+
1105
+ async function completeInlineOperation(
1106
+ db: Database,
1107
+ input: {
1108
+ accountId: string;
1109
+ workspaceId: string;
1110
+ subjectId: string;
1111
+ pluginKey: string;
1112
+ idempotencyKey: string;
1113
+ },
1114
+ requestDigest: string,
1115
+ result: Record<string, unknown>,
1116
+ ): Promise<void> {
1117
+ await db.insert(schema.capabilityOperations).values({
1118
+ accountId: input.accountId,
1119
+ workspaceId: input.workspaceId,
1120
+ idempotencyKey: input.idempotencyKey,
1121
+ requestDigest,
1122
+ kind: "uninstall",
1123
+ targetKind: "plugin",
1124
+ targetId: input.pluginKey,
1125
+ status: "completed",
1126
+ phase: "completed",
1127
+ result,
1128
+ createdBySubjectId: input.subjectId,
1129
+ completedAt: new Date(),
1130
+ });
1131
+ }
1132
+
1133
+ function objectValue(value: unknown): Record<string, unknown> {
1134
+ return value && typeof value === "object" && !Array.isArray(value)
1135
+ ? (value as Record<string, unknown>)
1136
+ : {};
1137
+ }
1138
+
1139
+ function stringValue(value: unknown): string | undefined {
1140
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1141
+ }
1142
+
1143
+ function stringArray(value: unknown): string[] {
1144
+ return Array.isArray(value)
1145
+ ? value.filter((entry): entry is string => typeof entry === "string")
1146
+ : [];
1147
+ }
1148
+
1149
+ function pluginBom(value: Record<string, unknown>): PluginBomComponent[] {
1150
+ if (!Array.isArray(value.bom)) return [];
1151
+ return value.bom.flatMap((entry) => {
1152
+ const record = objectValue(entry);
1153
+ const key = stringValue(record.key);
1154
+ const capabilityId = stringValue(record.capabilityId);
1155
+ const digest = stringValue(record.digest);
1156
+ const kind = record.kind;
1157
+ if (
1158
+ !key ||
1159
+ !capabilityId ||
1160
+ !digest ||
1161
+ !/^[0-9a-f]{64}$/.test(digest) ||
1162
+ (kind !== "skill" && kind !== "integration" && kind !== "mcp")
1163
+ ) {
1164
+ return [];
1165
+ }
1166
+ return [{ key, capabilityId, digest, kind }];
1167
+ });
1168
+ }
1169
+
1170
+ function safeHttpUrl(value: string): boolean {
1171
+ try {
1172
+ const url = new URL(value);
1173
+ return (
1174
+ (url.protocol === "https:" || url.protocol === "http:") &&
1175
+ !url.username &&
1176
+ !url.password &&
1177
+ !url.hash
1178
+ );
1179
+ } catch {
1180
+ return false;
1181
+ }
1182
+ }
1183
+
1184
+ function sha256(value: string): string {
1185
+ return createHash("sha256").update(value).digest("hex");
1186
+ }