@opengeni/db 5.0.1 → 6.0.0

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 (212) hide show
  1. package/dist/automations.d.ts +0 -2
  2. package/dist/canonical-human-identities.d.ts +4 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-components.d.ts +5 -13
  5. package/dist/capability-integrations.d.ts +23 -8
  6. package/dist/capability-reconciliation.d.ts +5 -0
  7. package/dist/capability-reconciliation.js +21 -0
  8. package/dist/capability-reconciliation.js.map +1 -0
  9. package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
  10. package/dist/chunk-4UJCXBR2.js.map +1 -0
  11. package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
  12. package/dist/chunk-AJA2ZIUW.js +219 -0
  13. package/dist/chunk-AJA2ZIUW.js.map +1 -0
  14. package/dist/chunk-CDAT6EKW.js +247 -0
  15. package/dist/chunk-CDAT6EKW.js.map +1 -0
  16. package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
  17. package/dist/chunk-CJI6G254.js.map +1 -0
  18. package/dist/chunk-G32NVMRJ.js +276 -0
  19. package/dist/chunk-G32NVMRJ.js.map +1 -0
  20. package/dist/chunk-GABXCG3N.js +5811 -0
  21. package/dist/chunk-GABXCG3N.js.map +1 -0
  22. package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
  23. package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
  24. package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
  25. package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
  26. package/dist/chunk-MN5PZMEK.js.map +1 -0
  27. package/dist/chunk-PJI4MUZ7.js +131 -0
  28. package/dist/chunk-PJI4MUZ7.js.map +1 -0
  29. package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
  30. package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
  31. package/dist/chunk-RL2MUXM3.js.map +1 -0
  32. package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
  33. package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
  34. package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
  35. package/dist/chunk-XIXVVBW2.js.map +1 -0
  36. package/dist/chunk-ZPJ6V2VG.js +156 -0
  37. package/dist/chunk-ZPJ6V2VG.js.map +1 -0
  38. package/dist/codex-capacity-recovery.d.ts +12 -0
  39. package/dist/codex-failure-eligibility.d.ts +9 -0
  40. package/dist/company-profile-schema.d.ts +4 -2
  41. package/dist/connect-attempts.d.ts +15 -0
  42. package/dist/connect-attempts.js +25 -0
  43. package/dist/connect-attempts.js.map +1 -0
  44. package/dist/connection-authority.d.ts +7 -63
  45. package/dist/connection-refresh-lock.d.ts +4 -0
  46. package/dist/connection-token-resolver.d.ts +8 -43
  47. package/dist/database-timing.d.ts +10 -0
  48. package/dist/database.d.ts +4 -0
  49. package/dist/editable-artifact-durable-export.js +2 -2
  50. package/dist/editable-artifacts.d.ts +9 -0
  51. package/dist/editable-artifacts.js +7 -5
  52. package/dist/index.d.ts +447 -78
  53. package/dist/index.js +12705 -12872
  54. package/dist/index.js.map +1 -1
  55. package/dist/insights-usage-bundle.d.ts +3 -0
  56. package/dist/integration-bindings.d.ts +1 -1
  57. package/dist/integration-facets.d.ts +18 -1
  58. package/dist/knowledge-entries-schema.d.ts +2 -0
  59. package/dist/knowledge-entries.d.ts +1 -0
  60. package/dist/knowledge-indexing.d.ts +1 -0
  61. package/dist/live-session-attempt.d.ts +35 -137
  62. package/dist/managed-auth-session-sets.js +3 -3
  63. package/dist/mcp-account-bindings.d.ts +4 -0
  64. package/dist/mcp-oauth.d.ts +11 -0
  65. package/dist/mcp-operations.js +2 -2
  66. package/dist/migrate.js +1 -1
  67. package/dist/organization-integration-policy.d.ts +51 -0
  68. package/dist/organization-integration-policy.js +18 -0
  69. package/dist/organization-integration-policy.js.map +1 -0
  70. package/dist/organization-usage.d.ts +20 -0
  71. package/dist/plugin-packages.d.ts +9 -12
  72. package/dist/pr-review.d.ts +0 -11
  73. package/dist/preference-registry-schema.d.ts +17 -0
  74. package/dist/provision-roles.js +1 -1
  75. package/dist/retained-provider-commands.d.ts +124 -6
  76. package/dist/retained-provider-commands.js +29 -97
  77. package/dist/retained-provider-commands.js.map +1 -1
  78. package/dist/retired-session-create-metadata.d.ts +4 -0
  79. package/dist/runtime-posture.d.ts +7 -5
  80. package/dist/schema.d.ts +876 -656
  81. package/dist/schema.js +5 -7
  82. package/dist/session-admission-block.d.ts +32 -0
  83. package/dist/session-attempt-writers.d.ts +4 -0
  84. package/dist/session-background-commands.js +5 -4
  85. package/dist/session-command-output.js +8 -126
  86. package/dist/session-command-output.js.map +1 -1
  87. package/dist/session-control.d.ts +5 -1
  88. package/dist/session-event-slices.d.ts +10 -0
  89. package/dist/session-event-slices.js +8 -222
  90. package/dist/session-event-slices.js.map +1 -1
  91. package/dist/session-file-attachments.d.ts +28 -0
  92. package/dist/session-goal-reports.d.ts +43 -0
  93. package/dist/session-mcp-approval.d.ts +4 -0
  94. package/dist/session-mcp-credential-rotation.js +5 -4
  95. package/dist/session-mcp-credential-rotation.js.map +1 -1
  96. package/dist/session-message-search.d.ts +22 -0
  97. package/dist/session-queue-commands.d.ts +2 -6
  98. package/dist/session-retry.d.ts +36 -0
  99. package/dist/session-tenancy.js +3 -3
  100. package/dist/skill-source-release-impact.d.ts +18 -0
  101. package/dist/skill-source-release.d.ts +9 -0
  102. package/dist/skills.d.ts +7 -1
  103. package/dist/user-resource-authority.d.ts +0 -8
  104. package/dist/video-generation.js +3 -3
  105. package/dist/workspace-tool-defaults.js +5 -4
  106. package/dist/workspace-tool-defaults.js.map +1 -1
  107. package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
  108. package/drizzle/0469_knowledge_source_discovery.sql +85 -0
  109. package/drizzle/0470_organization_integration_policy.sql +151 -0
  110. package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
  111. package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
  112. package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
  113. package/drizzle/0474_goal_report_requirements.sql +156 -0
  114. package/drizzle/0475_session_event_history_statistics.sql +15 -0
  115. package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
  116. package/drizzle/0477_managed_sign_in_methods.sql +303 -0
  117. package/drizzle/0478_sender_owned_connections.sql +2772 -0
  118. package/drizzle/0479_connection_create_idempotency.sql +16 -0
  119. package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
  120. package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
  121. package/drizzle/0482_remove_packs.sql +417 -0
  122. package/drizzle/0483_preclaim_admission_block.sql +85 -0
  123. package/drizzle/0484_insights_usage_projection.sql +211 -0
  124. package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
  125. package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
  126. package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
  127. package/drizzle/0488_permanent_skill_removal.sql +207 -0
  128. package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
  129. package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
  130. package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
  131. package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
  132. package/drizzle/0493_stopping_command_error_containment.sql +58 -0
  133. package/drizzle/0494_mcp_account_bindings.sql +334 -0
  134. package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
  135. package/drizzle/0496_supervised_command_settlement.sql +386 -0
  136. package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
  137. package/drizzle/0498_enrollment_membership_fence.sql +87 -0
  138. package/drizzle/0499_session_attachment_access.sql +224 -0
  139. package/drizzle/0500_session_execution_epoch.sql +37 -0
  140. package/drizzle/0501_session_sharing_execution.sql +591 -0
  141. package/drizzle/0502_active_message_boundary_forks.sql +30 -0
  142. package/package.json +17 -5
  143. package/src/automations.ts +17 -73
  144. package/src/canonical-human-identities.ts +17 -5
  145. package/src/capability-components.ts +7 -24
  146. package/src/capability-integrations.ts +301 -21
  147. package/src/capability-reconciliation.ts +19 -0
  148. package/src/codex-capacity-recovery.ts +47 -0
  149. package/src/codex-failure-eligibility.ts +93 -0
  150. package/src/connect-attempts.ts +49 -18
  151. package/src/connection-authority.ts +12 -158
  152. package/src/connection-refresh-lock.ts +22 -0
  153. package/src/connection-token-resolver.ts +58 -530
  154. package/src/database-timing.ts +43 -0
  155. package/src/database.ts +61 -26
  156. package/src/editable-artifacts.ts +1 -1
  157. package/src/index.ts +3578 -1273
  158. package/src/insights-model-bundle.ts +19 -7
  159. package/src/insights-usage-bundle.ts +16 -19
  160. package/src/integration-bindings.ts +2 -7
  161. package/src/integration-facets.ts +73 -6
  162. package/src/knowledge-entries.ts +8 -1
  163. package/src/live-session-attempt.ts +0 -22
  164. package/src/mcp-account-bindings.ts +12 -0
  165. package/src/mcp-oauth.ts +26 -0
  166. package/src/organization-integration-policy.ts +235 -0
  167. package/src/organization-usage.ts +83 -0
  168. package/src/plugin-packages.ts +226 -42
  169. package/src/pr-review.ts +7 -69
  170. package/src/preference-registry-schema.ts +1 -0
  171. package/src/provision-roles.ts +50 -37
  172. package/src/retained-provider-commands.ts +286 -9
  173. package/src/retired-session-create-metadata.ts +16 -0
  174. package/src/runtime-posture.ts +217 -13
  175. package/src/schema.ts +104 -120
  176. package/src/session-admission-block.ts +29 -0
  177. package/src/session-attempt-writers.ts +48 -0
  178. package/src/session-control.ts +21 -5
  179. package/src/session-event-slices.ts +76 -26
  180. package/src/session-file-attachments.ts +66 -0
  181. package/src/session-goal-reports.ts +255 -0
  182. package/src/session-mcp-approval.ts +30 -0
  183. package/src/session-message-search.ts +348 -0
  184. package/src/session-queue-commands.ts +60 -36
  185. package/src/session-retry.ts +319 -0
  186. package/src/skill-config-migration.ts +11 -2
  187. package/src/skill-source-release-impact.ts +33 -0
  188. package/src/skill-source-release.ts +67 -26
  189. package/src/skills.ts +44 -6
  190. package/src/user-resource-authority.ts +0 -56
  191. package/dist/chunk-6T2GGYWE.js.map +0 -1
  192. package/dist/chunk-7HMK3B45.js.map +0 -1
  193. package/dist/chunk-DPONPYJD.js.map +0 -1
  194. package/dist/chunk-F73UKJY6.js.map +0 -1
  195. package/dist/chunk-T6YLHJLS.js.map +0 -1
  196. package/dist/host-mcp-binding-match.d.ts +0 -5
  197. package/dist/host-mcp-bindings.d.ts +0 -79
  198. package/dist/host-mcp-resolvers.d.ts +0 -47
  199. package/dist/host-mcp-task-authority.d.ts +0 -43
  200. package/dist/host-selection-identity.d.ts +0 -6
  201. package/dist/pack-components.d.ts +0 -83
  202. package/dist/pack-installations.d.ts +0 -86
  203. package/src/host-mcp-binding-match.ts +0 -34
  204. package/src/host-mcp-bindings.ts +0 -777
  205. package/src/host-mcp-resolvers.ts +0 -266
  206. package/src/host-mcp-task-authority.ts +0 -384
  207. package/src/host-selection-identity.ts +0 -29
  208. package/src/pack-components.ts +0 -980
  209. package/src/pack-installations.ts +0 -950
  210. /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
  211. /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
  212. /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
@@ -0,0 +1,83 @@
1
+ import {
2
+ OrganizationUsageQuery,
3
+ OrganizationUsageSummary,
4
+ OrganizationUsageWorkspacePage,
5
+ OrganizationUsageWorkspacePageQuery,
6
+ type OrganizationUsagePeriod,
7
+ } from "@opengeni/contracts";
8
+ import { sql } from "drizzle-orm";
9
+ import { withAccountRls, type Database } from "./database";
10
+
11
+ /** Same UTC semantics as Insights: week is today and the previous six days. */
12
+ export function organizationUsageWindow(period: OrganizationUsagePeriod, now = new Date()) {
13
+ const until = new Date(now);
14
+ const since = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
15
+ if (period === "week") since.setUTCDate(since.getUTCDate() - 6);
16
+ if (period === "month") since.setUTCDate(1);
17
+ if (period === "ytd") since.setUTCMonth(0, 1);
18
+ if (!Number.isFinite(until.getTime())) throw new Error("Invalid usage window");
19
+ return {
20
+ since: since.toISOString(),
21
+ until: until.toISOString(),
22
+ granularity: period === "today" ? ("hour" as const) : ("day" as const),
23
+ };
24
+ }
25
+
26
+ type UsageInput = { accountId: string; period?: OrganizationUsagePeriod | undefined };
27
+
28
+ /** One set-based, actor-visible aggregate. Must use the writable primary. */
29
+ export async function getOrganizationUsageSummary(
30
+ db: Database,
31
+ input: UsageInput,
32
+ now = new Date(),
33
+ ): Promise<OrganizationUsageSummary> {
34
+ const query = OrganizationUsageQuery.parse(input);
35
+ const window = organizationUsageWindow(query.period, now);
36
+ const result = await readOrganizationUsage(db, input.accountId, window, true);
37
+ return OrganizationUsageSummary.parse({
38
+ ...result,
39
+ accountId: input.accountId,
40
+ period: query.period,
41
+ ...window,
42
+ });
43
+ }
44
+
45
+ /** Bounded page-only scan; never recomputes organization-wide totals/buckets. */
46
+ export async function getOrganizationUsageWorkspacePage(
47
+ db: Database,
48
+ input: UsageInput & { until: string; afterWorkspaceId?: string | undefined },
49
+ ): Promise<OrganizationUsageWorkspacePage> {
50
+ const query = OrganizationUsageWorkspacePageQuery.parse(input);
51
+ const window = organizationUsageWindow(query.period, new Date(query.until));
52
+ const result = await readOrganizationUsage(
53
+ db,
54
+ input.accountId,
55
+ window,
56
+ false,
57
+ query.afterWorkspaceId,
58
+ );
59
+ return OrganizationUsageWorkspacePage.parse({
60
+ ...result,
61
+ accountId: input.accountId,
62
+ period: query.period,
63
+ ...window,
64
+ });
65
+ }
66
+
67
+ async function readOrganizationUsage(
68
+ db: Database,
69
+ accountId: string,
70
+ window: ReturnType<typeof organizationUsageWindow>,
71
+ includePeriod: boolean,
72
+ afterWorkspaceId?: string,
73
+ ) {
74
+ return await withAccountRls(db, accountId, async (scopedDb) => {
75
+ await scopedDb.execute(sql`select set_config('statement_timeout', '10s', true)`);
76
+ const result = await scopedDb.execute(sql`select opengeni_private.organization_usage_summary(
77
+ ${accountId}::uuid, ${window.since}::timestamptz, ${window.until}::timestamptz,
78
+ ${window.granularity}::text, ${afterWorkspaceId ?? null}::uuid, ${includePeriod}::boolean
79
+ ) as summary`);
80
+ const rows = Array.isArray(result) ? result : (result as { rows: unknown[] }).rows;
81
+ return (rows[0] as { summary: Record<string, unknown> }).summary;
82
+ });
83
+ }
@@ -1,6 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import { stableJson, type CapabilityCatalogAuthKind, type SkillActor } from "@opengeni/contracts";
3
+ import {
4
+ stableJson,
5
+ type CapabilityCatalogAuthKind,
6
+ type SkillActor,
7
+ type PluginUninstallComponentImpact,
8
+ type PluginUninstallPreview,
9
+ } from "@opengeni/contracts";
4
10
  import { and, asc, eq, inArray, ne, or, sql } from "drizzle-orm";
5
11
 
6
12
  import {
@@ -8,8 +14,19 @@ import {
8
14
  cleanupOrphanedCapabilityComponents,
9
15
  lockCapabilityComponentIdentity,
10
16
  } from "./capability-components";
11
- import { setSubjectRlsContext, withRlsContext, withWorkspaceRls, type Database } from "./database";
12
- import type { SkillSourceReleaseReceipt } from "./skill-source-release";
17
+ import {
18
+ rawRows,
19
+ setSubjectRlsContext,
20
+ withRlsContext,
21
+ withWorkspaceRls,
22
+ type Database,
23
+ } from "./database";
24
+ import {
25
+ readSkillSourceReleaseHeads,
26
+ SkillSourceReleaseSnapshotChangedError,
27
+ type SkillSourceReleaseReceipt,
28
+ } from "./skill-source-release";
29
+ import { classifySkillSourceRelease } from "./skill-source-release-impact";
13
30
  import {
14
31
  removeIntegrationFacetBindingOwner,
15
32
  removeIntegrationFacetBindingOwnersForOwner,
@@ -21,6 +38,8 @@ import {
21
38
  readSkillPublications,
22
39
  } from "./skill-publication";
23
40
  import type { SkillPublicationReceipt } from "@opengeni/contracts";
41
+ import { withOrganizationIntegrationAcquisition } from "./organization-integration-policy";
42
+ import { nestedPostgresSqlState } from "./persistence-errors";
24
43
 
25
44
  export type PluginBomComponent = {
26
45
  key: string;
@@ -70,6 +89,14 @@ export class PluginInstallationVersionConflictError extends Error {
70
89
  readonly name = "PluginInstallationVersionConflictError";
71
90
  }
72
91
 
92
+ export class PluginUninstallPreviewChangedError extends Error {
93
+ readonly code = "plugin_uninstall_preview_changed";
94
+ constructor(readonly preview?: PluginUninstallPreview) {
95
+ super("Plugin removal impact changed. Review the refreshed preview before confirming again.");
96
+ this.name = "PluginUninstallPreviewChangedError";
97
+ }
98
+ }
99
+
73
100
  export class PluginInstallationVersionRequiredError extends Error {
74
101
  readonly name = "PluginInstallationVersionRequiredError";
75
102
  }
@@ -626,6 +653,16 @@ export async function installPluginMcpReference(
626
653
  authKind: CapabilityCatalogAuthKind;
627
654
  digest: string;
628
655
  },
656
+ ): Promise<{ capabilityId: string; facetInstallationId: string }> {
657
+ const snapshot = structuredClone(input);
658
+ return withOrganizationIntegrationAcquisition(db, snapshot, ["custom:mcp"], (tx) =>
659
+ installPluginMcpReferenceInScope(tx, snapshot),
660
+ );
661
+ }
662
+
663
+ async function installPluginMcpReferenceInScope(
664
+ db: Database,
665
+ input: Parameters<typeof installPluginMcpReference>[1],
629
666
  ): Promise<{ capabilityId: string; facetInstallationId: string }> {
630
667
  const capabilityId = `mcp:configured:${input.serverId}`;
631
668
  const pluginKey = `plugin-mcp/${input.serverId}`;
@@ -898,31 +935,21 @@ export async function getPluginPackageUninstallPreview(
898
935
  db: Database,
899
936
  workspaceId: string,
900
937
  pluginKey: string,
901
- ): Promise<{
902
- installed: boolean;
903
- version: string | null;
904
- installationVersion: number | null;
905
- components: Array<{
906
- capabilityId: string;
907
- kind: "skill" | "integration" | "mcp";
908
- retainedByOtherOwners: boolean;
909
- }>;
910
- }> {
938
+ subjectId?: string,
939
+ ): Promise<Omit<PluginUninstallPreview, "pluginKey">> {
911
940
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
941
+ if (subjectId) await setSubjectRlsContext(scopedDb, subjectId);
912
942
  const plugin = await pluginPackageInScope(scopedDb, workspaceId, pluginKey);
913
943
  if (!plugin || plugin.status === "disabled") {
914
944
  return { installed: false, version: null, installationVersion: null, components: [] };
915
945
  }
916
- const components = await pluginOwnedComponents(
917
- scopedDb,
918
- workspaceId,
919
- plugin.pluginInstallationId,
920
- );
946
+ const impact = await pluginOwnedComponents(scopedDb, workspaceId, plugin.pluginInstallationId);
921
947
  return {
922
948
  installed: true,
923
949
  version: plugin.version,
924
950
  installationVersion: plugin.installationVersion,
925
- components,
951
+ components: impact.components,
952
+ previewToken: pluginUninstallPreviewToken(plugin, impact.stateToken),
926
953
  };
927
954
  });
928
955
  }
@@ -935,6 +962,7 @@ export async function uninstallPluginPackage(
935
962
  subjectId: string;
936
963
  pluginKey: string;
937
964
  expectedInstallationVersion: number;
965
+ expectedPreviewToken?: string;
938
966
  idempotencyKey: string;
939
967
  skillActor?: SkillActor;
940
968
  },
@@ -947,6 +975,7 @@ export async function uninstallPluginPackage(
947
975
  stableJson({
948
976
  pluginKey: input.pluginKey,
949
977
  expectedInstallationVersion: input.expectedInstallationVersion,
978
+ ...(input.expectedPreviewToken ? { expectedPreviewToken: input.expectedPreviewToken } : {}),
950
979
  }),
951
980
  );
952
981
  return await withRlsContext(
@@ -956,6 +985,7 @@ export async function uninstallPluginPackage(
956
985
  await setSubjectRlsContext(scopedDb, input.subjectId);
957
986
  return await scopedDb.transaction(async (txRaw) => {
958
987
  const tx = txRaw as unknown as Database;
988
+ await lockSkillPublication(tx, input.workspaceId);
959
989
  await tx.execute(
960
990
  sql`select pg_advisory_xact_lock(hashtextextended(${`capability-operation:${input.workspaceId}:${input.idempotencyKey}`}, 0))`,
961
991
  );
@@ -1004,11 +1034,78 @@ export async function uninstallPluginPackage(
1004
1034
  if (plugin.installationVersion !== input.expectedInstallationVersion) {
1005
1035
  throw new PluginInstallationVersionConflictError("Plugin installation changed");
1006
1036
  }
1007
- const components = await pluginOwnedComponents(
1037
+ // Match cleanup/source-refresh ordering: child installations, then canonical
1038
+ // registry heads. Hold these locks through token comparison AND release.
1039
+ await tx.execute(sql`SELECT p.id FROM capability_plugin_installations p
1040
+ WHERE p.workspace_id=${input.workspaceId}::uuid AND p.id IN (
1041
+ SELECT fi.plugin_installation_id FROM capability_facet_installations fi
1042
+ JOIN capability_component_owners o ON o.facet_installation_id=fi.id
1043
+ WHERE o.workspace_id=${input.workspaceId}::uuid AND o.owner_kind='plugin'
1044
+ AND o.owner_id=${plugin.pluginInstallationId}
1045
+ ) ORDER BY p.id FOR UPDATE`);
1046
+ try {
1047
+ // Facet FK locks fence new owners; owner row locks fence releases by
1048
+
1049
+ // Those paths may remove an owner before acquiring the child lock, so
1050
+ // fail for a refreshed preview rather than creating a lock-order cycle.
1051
+ await tx.execute(sql`SELECT fi.id FROM capability_facet_installations fi
1052
+ WHERE fi.workspace_id=${input.workspaceId}::uuid AND EXISTS (
1053
+ SELECT 1 FROM capability_component_owners own
1054
+ WHERE own.facet_installation_id=fi.id AND own.owner_kind='plugin'
1055
+ AND own.owner_id=${plugin.pluginInstallationId}
1056
+ ) ORDER BY fi.id FOR UPDATE NOWAIT`);
1057
+ await tx.execute(sql`SELECT o.id FROM capability_component_owners o
1058
+ WHERE o.workspace_id=${input.workspaceId}::uuid AND EXISTS (
1059
+ SELECT 1 FROM capability_component_owners own
1060
+ WHERE own.facet_installation_id=o.facet_installation_id
1061
+ AND own.owner_kind='plugin' AND own.owner_id=${plugin.pluginInstallationId}
1062
+ ) ORDER BY o.id FOR UPDATE NOWAIT`);
1063
+ } catch (error) {
1064
+ if (nestedPostgresSqlState(error) === "55P03")
1065
+ throw new PluginUninstallPreviewChangedError();
1066
+ throw error;
1067
+ }
1068
+ const skillBindings = await rawRows<{ id: string }>(
1069
+ tx,
1070
+ sql`
1071
+ SELECT DISTINCT h.id FROM skill_source_bindings b
1072
+ JOIN capability_facet_installations fi ON fi.facet_id=b.skill_facet_id
1073
+ AND fi.account_id=b.account_id AND fi.workspace_id=b.workspace_id
1074
+ JOIN capability_component_owners o ON o.facet_installation_id=fi.id
1075
+ JOIN preference_registry_preferences h ON h.id=b.preference_id AND h.account_id=b.account_id
1076
+ WHERE b.workspace_id=${input.workspaceId}::uuid AND o.owner_kind='plugin'
1077
+ AND o.owner_id=${plugin.pluginInstallationId} ORDER BY h.id`,
1078
+ );
1079
+ for (const head of skillBindings) {
1080
+ try {
1081
+ await tx.execute(
1082
+ sql`SELECT preference_id FROM preference_registry_lock_heads(ARRAY[${head.id}::uuid])`,
1083
+ );
1084
+ } catch (error) {
1085
+ // A concurrent scope move can make the selected head invisible while
1086
+ // this lock waits. Roll back and obtain the refreshed preview outside
1087
+ // the failed transaction, without disclosing the hidden head.
1088
+ if (nestedPostgresSqlState(error) === "42501")
1089
+ throw new PluginUninstallPreviewChangedError();
1090
+ throw error;
1091
+ }
1092
+ }
1093
+ const impact = await pluginOwnedComponents(
1008
1094
  tx,
1009
1095
  input.workspaceId,
1010
1096
  plugin.pluginInstallationId,
1011
1097
  );
1098
+ const previewToken = pluginUninstallPreviewToken(plugin, impact.stateToken);
1099
+ if (input.expectedPreviewToken && input.expectedPreviewToken !== previewToken) {
1100
+ throw new PluginUninstallPreviewChangedError({
1101
+ pluginKey: input.pluginKey,
1102
+ installed: true,
1103
+ version: plugin.version,
1104
+ installationVersion: plugin.installationVersion,
1105
+ previewToken,
1106
+ components: impact.components,
1107
+ });
1108
+ }
1012
1109
  await removeIntegrationFacetBindingOwnersForOwner(tx, {
1013
1110
  workspaceId: input.workspaceId,
1014
1111
  owner: { kind: "plugin", id: plugin.pluginInstallationId },
@@ -1023,12 +1120,20 @@ export async function uninstallPluginPackage(
1023
1120
  ),
1024
1121
  )
1025
1122
  .returning({ facetInstallationId: schema.capabilityComponentOwners.facetInstallationId });
1026
- const skillReleases = await cleanupOrphanedCapabilityComponents(
1027
- tx,
1028
- input.workspaceId,
1029
- owned.map((row) => row.facetInstallationId),
1030
- input.skillActor,
1031
- );
1123
+ let skillReleases: SkillSourceReleaseReceipt[];
1124
+ try {
1125
+ skillReleases = await cleanupOrphanedCapabilityComponents(
1126
+ tx,
1127
+ input.workspaceId,
1128
+ owned.map((row) => row.facetInstallationId),
1129
+ input.skillActor,
1130
+ skillBindings.map((head) => head.id),
1131
+ );
1132
+ } catch (error) {
1133
+ if (error instanceof SkillSourceReleaseSnapshotChangedError)
1134
+ throw new PluginUninstallPreviewChangedError();
1135
+ throw error;
1136
+ }
1032
1137
  await tx
1033
1138
  .update(schema.capabilityPluginInstallations)
1034
1139
  .set({
@@ -1037,8 +1142,8 @@ export async function uninstallPluginPackage(
1037
1142
  updatedAt: new Date(),
1038
1143
  })
1039
1144
  .where(eq(schema.capabilityPluginInstallations.id, plugin.pluginInstallationId));
1040
- const retainedComponents = components
1041
- .filter((component) => component.retainedByOtherOwners)
1145
+ const retainedComponents = impact.components
1146
+ .filter((component) => component.disposition === "retained")
1042
1147
  .map((component) => component.capabilityId);
1043
1148
  await completeInlineOperation(tx, input, requestDigest, {
1044
1149
  status: "uninstalled",
@@ -1097,18 +1202,13 @@ async function pluginOwnedComponents(
1097
1202
  db: Database,
1098
1203
  workspaceId: string,
1099
1204
  ownerPluginInstallationId: string,
1100
- ): Promise<
1101
- Array<{
1102
- capabilityId: string;
1103
- kind: "skill" | "integration" | "mcp";
1104
- retainedByOtherOwners: boolean;
1105
- }>
1106
- > {
1205
+ ): Promise<{ components: PluginUninstallComponentImpact[]; stateToken: string }> {
1107
1206
  const rows = await db
1108
1207
  .select({
1109
1208
  facetInstallationId: schema.capabilityFacetInstallations.id,
1110
1209
  facetKind: schema.capabilityFacets.kind,
1111
1210
  skillCapabilityId: schema.capabilitySkillFacets.capabilityId,
1211
+ name: schema.capabilityPlugins.name,
1112
1212
  manifest: schema.capabilityPluginVersions.manifest,
1113
1213
  })
1114
1214
  .from(schema.capabilityComponentOwners)
@@ -1134,6 +1234,10 @@ async function pluginOwnedComponents(
1134
1234
  schema.capabilityPluginVersions,
1135
1235
  eq(schema.capabilityPluginVersions.id, schema.capabilityPluginInstallations.pluginVersionId),
1136
1236
  )
1237
+ .innerJoin(
1238
+ schema.capabilityPlugins,
1239
+ eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
1240
+ )
1137
1241
  .leftJoin(
1138
1242
  schema.capabilitySkillFacets,
1139
1243
  eq(schema.capabilitySkillFacets.facetId, schema.capabilityFacets.id),
@@ -1148,7 +1252,12 @@ async function pluginOwnedComponents(
1148
1252
  .orderBy(asc(schema.capabilityFacets.kind), asc(schema.capabilityFacets.facetKey));
1149
1253
  const components = new Map<
1150
1254
  string,
1151
- { capabilityId: string; kind: "skill" | "integration" | "mcp"; facetIds: string[] }
1255
+ {
1256
+ capabilityId: string;
1257
+ name: string;
1258
+ kind: "skill" | "integration" | "mcp";
1259
+ facetIds: string[];
1260
+ }
1152
1261
  >();
1153
1262
  for (const row of rows) {
1154
1263
  const manifest = objectValue(row.manifest);
@@ -1164,15 +1273,26 @@ async function pluginOwnedComponents(
1164
1273
  if (!capabilityId || !kind) continue;
1165
1274
  const existing = components.get(capabilityId);
1166
1275
  if (existing) existing.facetIds.push(row.facetInstallationId);
1167
- else components.set(capabilityId, { capabilityId, kind, facetIds: [row.facetInstallationId] });
1276
+ else
1277
+ components.set(capabilityId, {
1278
+ capabilityId,
1279
+ kind,
1280
+ name: row.name,
1281
+ facetIds: [row.facetInstallationId],
1282
+ });
1168
1283
  }
1169
- const result = [];
1284
+ const result: PluginUninstallComponentImpact[] = [];
1285
+ const state: unknown[] = [];
1170
1286
  for (const component of components.values()) {
1171
- const [ownerCount] = await db
1172
- .select({ count: sql<number>`count(*)::int` })
1287
+ const owners = await db
1288
+ .select({
1289
+ kind: schema.capabilityComponentOwners.ownerKind,
1290
+ id: schema.capabilityComponentOwners.ownerId,
1291
+ })
1173
1292
  .from(schema.capabilityComponentOwners)
1174
1293
  .where(
1175
1294
  and(
1295
+ eq(schema.capabilityComponentOwners.workspaceId, workspaceId),
1176
1296
  inArray(schema.capabilityComponentOwners.facetInstallationId, component.facetIds),
1177
1297
  // Retention is deliberately physical, not runtime-effective. A Plugin
1178
1298
  // in needs_attention may be resumable and must keep shared component
@@ -1185,13 +1305,77 @@ async function pluginOwnedComponents(
1185
1305
  ),
1186
1306
  ),
1187
1307
  );
1308
+ const remainingOwners: PluginUninstallComponentImpact["remainingOwners"] = [];
1309
+ for (const owner of owners.sort((a, b) =>
1310
+ `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`),
1311
+ )) {
1312
+ let name =
1313
+ owner.kind === "direct"
1314
+ ? "Direct installation"
1315
+ : owner.kind === "migration"
1316
+ ? "Migrated installation"
1317
+ : "Another installed owner";
1318
+ if (owner.kind === "plugin") {
1319
+ const [named] = await db
1320
+ .select({ manifest: schema.capabilityPluginVersions.manifest })
1321
+ .from(schema.capabilityPluginInstallations)
1322
+ .innerJoin(
1323
+ schema.capabilityPluginVersions,
1324
+ eq(
1325
+ schema.capabilityPluginVersions.id,
1326
+ schema.capabilityPluginInstallations.pluginVersionId,
1327
+ ),
1328
+ )
1329
+ .where(
1330
+ and(
1331
+ eq(schema.capabilityPluginInstallations.workspaceId, workspaceId),
1332
+ sql`${schema.capabilityPluginInstallations.id}::text = ${owner.id}`,
1333
+ ),
1334
+ );
1335
+ name = stringValue(objectValue(named?.manifest).name) ?? name;
1336
+ }
1337
+ if (!remainingOwners.some((item) => item.kind === owner.kind && item.name === name))
1338
+ remainingOwners.push({
1339
+ kind: owner.kind as PluginUninstallComponentImpact["remainingOwners"][number]["kind"],
1340
+ name,
1341
+ });
1342
+ }
1343
+ const heads =
1344
+ component.kind === "skill"
1345
+ ? await readSkillSourceReleaseHeads(db, workspaceId, component.facetIds)
1346
+ : [];
1347
+ const head = heads[0];
1348
+ const skillImpact = head ? classifySkillSourceRelease(head, workspaceId) : null;
1349
+ const retainedByOtherOwners = owners.length > 0;
1350
+ const retentionReasons: PluginUninstallComponentImpact["retentionReasons"] = [];
1351
+ if (retainedByOtherOwners) retentionReasons.push("other_owners");
1352
+ if (skillImpact) retentionReasons.push(...skillImpact.retentionReasons);
1353
+ else if (component.kind === "skill") retentionReasons.push("registry_unavailable");
1354
+ state.push({ facetIds: component.facetIds, owners, heads });
1188
1355
  result.push({
1189
1356
  capabilityId: component.capabilityId,
1190
1357
  kind: component.kind,
1191
- retainedByOtherOwners: (ownerCount?.count ?? 0) > 0,
1358
+ name: head?.title ?? component.name,
1359
+ retainedByOtherOwners,
1360
+ disposition: retainedByOtherOwners
1361
+ ? "retained"
1362
+ : (skillImpact?.disposition ?? (component.kind === "skill" ? "retained" : "removed")),
1363
+ retentionReasons,
1364
+ remainingOwners,
1365
+ ...(head ? { skillId: head.id } : {}),
1192
1366
  });
1193
1367
  }
1194
- return result;
1368
+ return { components: result, stateToken: sha256(stableJson({ state, components: result })) };
1369
+ }
1370
+
1371
+ function pluginUninstallPreviewToken(plugin: InstalledPluginPackage, stateToken: string): string {
1372
+ return sha256(
1373
+ stableJson({
1374
+ installationId: plugin.pluginInstallationId,
1375
+ version: plugin.installationVersion,
1376
+ stateToken,
1377
+ }),
1378
+ );
1195
1379
  }
1196
1380
 
1197
1381
  async function completeInlineOperation(
package/src/pr-review.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  AutomationSessionTemplate,
3
- OPENGENI_PR_REVIEW_PACK_ID,
4
3
  OPENGENI_PR_REVIEW_SESSION_ROLE,
5
4
  type PrReviewAppRegistration,
6
5
  type GitHubRepository,
@@ -43,8 +42,6 @@ export async function createPrReviewAppRegistration(
43
42
  webhookSecretEncrypted: string;
44
43
  webhookUsername: string | null;
45
44
  createdBySubjectId: string;
46
- packInstallationId: string;
47
- packConnectorId: string;
48
45
  },
49
46
  ): Promise<PrReviewAppRegistration> {
50
47
  return await withRlsContext(db, input, async (scopedDb) => {
@@ -64,8 +61,7 @@ export async function createPrReviewAppRegistration(
64
61
  webhookUsername: input.webhookUsername,
65
62
  },
66
63
  webhookSecretEncrypted: input.webhookSecretEncrypted,
67
- packInstallationId: input.packInstallationId,
68
- packConnectorId: input.packConnectorId,
64
+
69
65
  createdBySubjectId: input.createdBySubjectId,
70
66
  })
71
67
  .returning();
@@ -312,8 +308,7 @@ export async function createPrReviewRepositoryBinding(
312
308
  additionalInstructions: string | null;
313
309
  status: "active" | "disabled";
314
310
  createdBySubjectId: string;
315
- packInstallationId: string;
316
- packTemplateId: string;
311
+
317
312
  adapterId: string;
318
313
  eventTypes: string[];
319
314
  configuration: Record<string, unknown>;
@@ -357,8 +352,7 @@ export async function createPrReviewRepositoryBinding(
357
352
  sourceId: registration.sourceId,
358
353
  name: `Review ${input.repositoryFullName}`,
359
354
  status: input.status,
360
- packInstallationId: input.packInstallationId,
361
- packTemplateId: input.packTemplateId,
355
+
362
356
  createdBySubjectId: input.createdBySubjectId,
363
357
  })
364
358
  .returning();
@@ -401,10 +395,6 @@ export async function createPrReviewRepositoryBinding(
401
395
  });
402
396
  }
403
397
 
404
- /** Atomically converge one owner-authorized installation of the deployment
405
- * review App onto the Pack's ordinary source/trigger/binding model. Re-running
406
- * the browser flow repairs disabled rows, follows GitHub repository selection,
407
- * and removes shared-webhook routes for repositories no longer selected. */
408
398
  export async function syncManagedGitHubPrReviewInstallation(
409
399
  db: Database,
410
400
  input: {
@@ -422,9 +412,7 @@ export async function syncManagedGitHubPrReviewInstallation(
422
412
  webhookSecretEncrypted: string;
423
413
  repositories: GitHubRepository[];
424
414
  createdBySubjectId: string;
425
- packInstallationId: string;
426
- packConnectorId: string;
427
- packTemplateId: string;
415
+
428
416
  adapterId: string;
429
417
  eventTypes: string[];
430
418
  configuration: Record<string, unknown>;
@@ -439,22 +427,6 @@ export async function syncManagedGitHubPrReviewInstallation(
439
427
  input,
440
428
  async (scopedDb) =>
441
429
  await scopedDb.transaction(async (tx) => {
442
- const [packInstallation] = await tx
443
- .select({ id: schema.packInstallations.id })
444
- .from(schema.packInstallations)
445
- .where(
446
- and(
447
- eq(schema.packInstallations.workspaceId, input.workspaceId),
448
- eq(schema.packInstallations.id, input.packInstallationId),
449
- eq(schema.packInstallations.packId, OPENGENI_PR_REVIEW_PACK_ID),
450
- eq(schema.packInstallations.status, "active"),
451
- ),
452
- )
453
- .limit(1)
454
- .for("update");
455
- if (!packInstallation) {
456
- throw new PrReviewDispatchAuthorityError("OpenGeni Review Bot Pack is not active");
457
- }
458
430
  const installationId = String(input.installationId);
459
431
  const [nonceReceipt] = await tx
460
432
  .insert(schema.prReviewManagedGithubAuthorityNonces)
@@ -507,8 +479,7 @@ export async function syncManagedGitHubPrReviewInstallation(
507
479
  configuration: nextSourceConfiguration,
508
480
  webhookSecretEncrypted: input.webhookSecretEncrypted,
509
481
  status: "active",
510
- packInstallationId: input.packInstallationId,
511
- packConnectorId: input.packConnectorId,
482
+
512
483
  version: sql`${schema.automationSources.version} + 1`,
513
484
  updatedAt: new Date(),
514
485
  })
@@ -559,8 +530,7 @@ export async function syncManagedGitHubPrReviewInstallation(
559
530
  webhookUsername: null,
560
531
  },
561
532
  webhookSecretEncrypted: input.webhookSecretEncrypted,
562
- packInstallationId: input.packInstallationId,
563
- packConnectorId: input.packConnectorId,
533
+
564
534
  createdBySubjectId: input.createdBySubjectId,
565
535
  })
566
536
  .returning();
@@ -655,8 +625,7 @@ export async function syncManagedGitHubPrReviewInstallation(
655
625
  sourceId: source.id,
656
626
  name: `Review ${repository.fullName}`,
657
627
  status: "active",
658
- packInstallationId: input.packInstallationId,
659
- packTemplateId: input.packTemplateId,
628
+
660
629
  createdBySubjectId: input.createdBySubjectId,
661
630
  })
662
631
  .returning();
@@ -1079,10 +1048,6 @@ export async function resolvePrReviewGitCredential(
1079
1048
  runSessionId: schema.automationRuns.sessionId,
1080
1049
  triggerStatus: schema.automationTriggers.status,
1081
1050
  sourceStatus: schema.automationSources.status,
1082
- sourcePackInstallationId: schema.automationSources.packInstallationId,
1083
- sourcePackConnectorId: schema.automationSources.packConnectorId,
1084
- packInstallationId: schema.packInstallations.id,
1085
- packStatus: schema.packInstallations.status,
1086
1051
  bindingStatus: schema.prReviewRepositoryBindings.status,
1087
1052
  registrationStatus: schema.prReviewAppRegistrations.status,
1088
1053
  })
@@ -1116,13 +1081,6 @@ export async function resolvePrReviewGitCredential(
1116
1081
  eq(schema.prReviewAppRegistrations.sourceId, schema.automationRuns.sourceId),
1117
1082
  ),
1118
1083
  )
1119
- .innerJoin(
1120
- schema.packInstallations,
1121
- and(
1122
- eq(schema.packInstallations.workspaceId, schema.automationRuns.workspaceId),
1123
- eq(schema.packInstallations.id, schema.automationTriggers.packInstallationId),
1124
- ),
1125
- )
1126
1084
  .where(
1127
1085
  and(
1128
1086
  eq(schema.automationRuns.workspaceId, input.workspaceId),
@@ -1131,7 +1089,6 @@ export async function resolvePrReviewGitCredential(
1131
1089
  eq(schema.automationRuns.sessionId, input.sessionId),
1132
1090
  eq(schema.prReviewRepositoryBindings.id, bindingId),
1133
1091
  eq(schema.prReviewAppRegistrations.id, input.registrationId),
1134
- eq(schema.packInstallations.packId, OPENGENI_PR_REVIEW_PACK_ID),
1135
1092
  ),
1136
1093
  )
1137
1094
  .limit(1);
@@ -1141,9 +1098,6 @@ export async function resolvePrReviewGitCredential(
1141
1098
  execution.runSessionId !== input.sessionId ||
1142
1099
  execution.triggerStatus !== "active" ||
1143
1100
  execution.sourceStatus !== "active" ||
1144
- execution.sourcePackInstallationId !== execution.packInstallationId ||
1145
- execution.sourcePackConnectorId !== prReviewProviderPackConnectorId(input.provider) ||
1146
- execution.packStatus !== "active" ||
1147
1101
  execution.bindingStatus !== "active" ||
1148
1102
  execution.registrationStatus !== "active"
1149
1103
  ) {
@@ -1194,18 +1148,6 @@ export async function resolvePrReviewGitCredential(
1194
1148
  "PR Review credential is not authorized for this repository",
1195
1149
  );
1196
1150
  }
1197
- const [pack] = await scopedDb
1198
- .select({ status: schema.packInstallations.status })
1199
- .from(schema.packInstallations)
1200
- .where(
1201
- and(
1202
- eq(schema.packInstallations.workspaceId, input.workspaceId),
1203
- eq(schema.packInstallations.packId, OPENGENI_PR_REVIEW_PACK_ID),
1204
- ),
1205
- )
1206
- .limit(1);
1207
- if (pack?.status !== "active")
1208
- throw new PrReviewDispatchAuthorityError("OpenGeni Review Bot Pack is not active");
1209
1151
  return {
1210
1152
  credentialKind: registration.credentialKind as
1211
1153
  | "github_app"
@@ -1218,10 +1160,6 @@ export async function resolvePrReviewGitCredential(
1218
1160
  });
1219
1161
  }
1220
1162
 
1221
- function prReviewProviderPackConnectorId(provider: PrReviewProvider): string {
1222
- return provider === "azure_devops" ? "azure-devops" : provider;
1223
- }
1224
-
1225
1163
  function prReviewMetadataText(metadata: unknown, key: string): string | null {
1226
1164
  if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) return null;
1227
1165
  const value = (metadata as Record<string, unknown>)[key];
@@ -80,6 +80,7 @@ export const preferenceRegistryRevisions = pgTable(
80
80
  contentHash: text("content_hash").notNull(),
81
81
  skillFiles: jsonb("skill_files").$type<Array<{ path: string; content: string }> | null>(),
82
82
  skillActivationMode: text("skill_activation_mode"),
83
+ skillRemovalOperationId: uuid("skill_removal_operation_id"),
83
84
  precedenceRank: integer("precedence_rank").notNull().default(0),
84
85
  conflictStrategy: text("conflict_strategy").notNull(),
85
86
  conflictsWith: jsonb("conflicts_with").$type<string[]>().notNull().default([]),