@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
@@ -1,6 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import { stableJson, type McpServerConnectionRef } from "@opengeni/contracts";
3
+ import {
4
+ assertOrganizationIntegrationAllowed,
5
+ stableJson,
6
+ type IntegrationSource,
7
+ type McpServerConnectionRef,
8
+ } from "@opengeni/contracts";
4
9
  import { and, asc, eq, inArray, ne, or, sql } from "drizzle-orm";
5
10
 
6
11
  import {
@@ -16,6 +21,7 @@ import {
16
21
  type Database,
17
22
  } from "./database";
18
23
  import { connectionScopeKey } from "./connection-scopes";
24
+ import { withOrganizationIntegrationPolicyFence } from "./organization-integration-policy";
19
25
  import {
20
26
  ensureIntegrationFacetDefinition,
21
27
  integrationBindingKey,
@@ -184,7 +190,7 @@ export type ApiIntegrationRuntime = {
184
190
  };
185
191
 
186
192
  export type ApiIntegrationOwner = {
187
- kind: "direct" | "plugin" | "pack" | "migration";
193
+ kind: "direct" | "plugin" | "migration";
188
194
  id: string;
189
195
  removable: boolean;
190
196
  };
@@ -227,6 +233,25 @@ export class ApiIntegrationInstallationVersionConflictError extends Error {
227
233
  export async function installApiIntegration(
228
234
  db: Database,
229
235
  input: InstallApiIntegrationInput,
236
+ ): Promise<InstalledApiIntegration> {
237
+ // Inputs here are server-resolved immutable previews, not deserialized client
238
+ // provenance. Custom documents never gain curated permission by their URL.
239
+ const snapshot = structuredClone(input);
240
+ const key =
241
+ snapshot.definitionProvenance === "curated"
242
+ ? snapshot.definitionId
243
+ : `custom:${snapshot.protocol}`;
244
+ return withOrganizationIntegrationPolicyFence(db, snapshot, (tx, policy) =>
245
+ installApiIntegrationInScope(tx, snapshot, () =>
246
+ assertOrganizationIntegrationAllowed(policy, key),
247
+ ),
248
+ );
249
+ }
250
+
251
+ async function installApiIntegrationInScope(
252
+ db: Database,
253
+ input: InstallApiIntegrationInput,
254
+ assertAcquisition: () => void,
230
255
  ): Promise<InstalledApiIntegration> {
231
256
  assertInstallInput(input);
232
257
  return await withRlsContext(
@@ -407,6 +432,12 @@ export async function installApiIntegration(
407
432
  const displayName =
408
433
  input.displayName?.trim() ||
409
434
  (instanceKey === "default" ? input.name : `${input.name} — connected account`);
435
+ // Capture immutable definition identity before any ensure helper can
436
+ // repair it. A repair/addition is acquisition, not unchanged replay.
437
+ const previousDefinitions = await tx
438
+ .select()
439
+ .from(schema.integrationFacetDefinitions)
440
+ .where(eq(schema.integrationFacetDefinitions.integrationFacetId, integrationFacet.id));
410
441
  const toolsFacet = await ensureIntegrationFacetDefinition(tx as unknown as Database, {
411
442
  integrationFacetId: integrationFacet.id,
412
443
  facetKey: "tools",
@@ -536,6 +567,116 @@ export async function installApiIntegration(
536
567
  const approvalRequiredTools = input.revision.tools
537
568
  .filter((tool) => selectedTools.includes(tool.id) && tool.approvalMode === "ask")
538
569
  .map((tool) => tool.id);
570
+ const nextConfig = {
571
+ baseServerId: input.serverId,
572
+ allowedTools: selectedTools,
573
+ requireApproval: approvalRequiredTools,
574
+ connectionKind: connection?.kind ?? null,
575
+ subjectScope: connection?.subjectId ? "subject" : connection ? "workspace" : "none",
576
+ };
577
+ // This is the same advisory -> row prefix used by the binding upsert.
578
+ // All preceding ensure writes are local to this transaction and roll
579
+ // back if admission below denies a new acquisition.
580
+ await tx.execute(
581
+ sql`select pg_advisory_xact_lock(hashtextextended(${`integration-binding:${input.workspaceId}:${runtimeServerId}`}, 0))`,
582
+ );
583
+ const [previousBinding] = await tx
584
+ .select()
585
+ .from(schema.integrationFacetBindings)
586
+ .where(
587
+ and(
588
+ eq(schema.integrationFacetBindings.workspaceId, input.workspaceId),
589
+ eq(schema.integrationFacetBindings.runtimeKey, runtimeServerId),
590
+ ),
591
+ )
592
+ .for("update")
593
+ .limit(1);
594
+ const bindingOwners = previousBinding
595
+ ? await listIntegrationFacetBindingOwners(tx as unknown as Database, previousBinding.id)
596
+ : [];
597
+ const componentOwners = await tx
598
+ .select()
599
+ .from(schema.capabilityComponentOwners)
600
+ .where(
601
+ inArray(schema.capabilityComponentOwners.facetInstallationId, [
602
+ integrationFacetInstallation.id,
603
+ apiFacetInstallation.id,
604
+ ]),
605
+ );
606
+ const [storedSpec] = await tx
607
+ .select()
608
+ .from(schema.integrationSpecRevisions)
609
+ .where(
610
+ and(
611
+ eq(schema.integrationSpecRevisions.apiFacetId, apiFacet.id),
612
+ eq(schema.integrationSpecRevisions.status, "active"),
613
+ ),
614
+ )
615
+ .limit(1);
616
+ const [storedAuthority] = await tx
617
+ .select()
618
+ .from(schema.capabilityIntegrationFacets)
619
+ .where(eq(schema.capabilityIntegrationFacets.facetId, integrationFacet.id))
620
+ .limit(1);
621
+ const [storedApi] = await tx
622
+ .select()
623
+ .from(schema.capabilityApiFacets)
624
+ .where(eq(schema.capabilityApiFacets.facetId, apiFacet.id))
625
+ .limit(1);
626
+ const sameOwner = (candidate: { kind: string; id: string; removable: boolean }) =>
627
+ candidate.kind === owner.kind &&
628
+ candidate.id === owner.id &&
629
+ candidate.removable === owner.removable;
630
+ const unchangedAuthority =
631
+ existingPluginInstallation?.status === "active" &&
632
+ existingPluginInstallation.pluginVersionId === pluginVersion.id &&
633
+ !integrationFacetInstallationResult.changed &&
634
+ !apiFacetInstallationResult.changed &&
635
+ previousBinding?.status === "active" &&
636
+ previousBinding.bindingKey === instanceKey &&
637
+ previousBinding.integrationFacetInstallationId === integrationFacetInstallation.id &&
638
+ previousBinding.facetDefinitionId === toolsFacet.id &&
639
+ previousBinding.connectionId === (input.connectionId ?? null) &&
640
+ isApiIntegrationPermissionReduction(previousBinding.config, nextConfig) &&
641
+ bindingOwners.some(sameOwner) &&
642
+ [integrationFacetInstallation.id, apiFacetInstallation.id].every((id) =>
643
+ componentOwners.some(
644
+ (candidate) =>
645
+ candidate.facetInstallationId === id &&
646
+ sameOwner({
647
+ kind: candidate.ownerKind,
648
+ id: candidate.ownerId,
649
+ removable: candidate.removable,
650
+ }),
651
+ ),
652
+ ) &&
653
+ stableJson(storedSpec?.spec) === stableJson(input.revision) &&
654
+ storedApi?.protocol === input.protocol &&
655
+ storedApi.baseUrl === input.baseUrl &&
656
+ storedApi.specSourceUrl === (input.sourceUrl ?? null) &&
657
+ storedAuthority?.providerDomain === input.providerDomain &&
658
+ stableJson(storedApi?.authScheme) === stableJson(input.authScheme ?? {}) &&
659
+ stableJson(storedAuthority?.requiredScopes) ===
660
+ stableJson(normalizedStrings(input.requiredScopes ?? [], 256)) &&
661
+ storedAuthority?.ownership ===
662
+ (input.ownership ?? (connection?.subjectId ? "subject" : "workspace")) &&
663
+ previousDefinitions.some(
664
+ (definition) =>
665
+ definition.id === toolsFacet.id &&
666
+ definition.kind === toolsFacet.kind &&
667
+ stableJson(definition.configSchema) === stableJson(toolsFacet.configSchema) &&
668
+ stableJson(definition.capabilities) === stableJson(toolsFacet.capabilities),
669
+ ) &&
670
+ (input.facetDefinitions ?? []).every((definition) =>
671
+ previousDefinitions.some(
672
+ (old) =>
673
+ old.facetKey === definition.facetKey &&
674
+ old.kind === definition.kind &&
675
+ stableJson(old.configSchema) === stableJson(definition.configSchema) &&
676
+ stableJson(old.capabilities) === stableJson(definition.capabilities),
677
+ ),
678
+ );
679
+ if (!unchangedAuthority) assertAcquisition();
539
680
  const bindingResult = await upsertIntegrationFacetBinding(tx as unknown as Database, {
540
681
  accountId: input.accountId,
541
682
  workspaceId: input.workspaceId,
@@ -545,13 +686,7 @@ export async function installApiIntegration(
545
686
  displayName,
546
687
  runtimeKey: runtimeServerId,
547
688
  connectionId: input.connectionId ?? null,
548
- config: {
549
- baseServerId: input.serverId,
550
- allowedTools: selectedTools,
551
- requireApproval: approvalRequiredTools,
552
- connectionKind: connection?.kind ?? null,
553
- subjectScope: connection?.subjectId ? "subject" : connection ? "workspace" : "none",
554
- },
689
+ config: nextConfig,
555
690
  createdBySubjectId: input.subjectId,
556
691
  owner,
557
692
  ...(input.expectedInstanceVersion !== undefined
@@ -812,12 +947,6 @@ export async function listInstalledApiIntegrationServerIdsForDelegations(
812
947
  return [...byServerId];
813
948
  }
814
949
 
815
- /**
816
- * Read installed API Integrations from a transaction that already carries the
817
- * workspace RLS context. This is intentionally separate from the public
818
- * wrapper so compound Plugin/Pack lifecycle transactions never open a nested
819
- * transaction or lose their advisory locks.
820
- */
821
950
  export async function listInstalledApiIntegrationsInRlsContext(
822
951
  scopedDb: Database,
823
952
  workspaceId: string,
@@ -1078,6 +1207,133 @@ export async function getApiIntegrationUninstallPreview(
1078
1207
  });
1079
1208
  }
1080
1209
 
1210
+ /** No discovery or new authority: recover only an exact, currently visible and
1211
+ * directly owned installed source. The installer must still lock and recheck
1212
+ * the snapshot before committing; this read is not installation admission.
1213
+ */
1214
+ export async function getApiIntegrationReconciliationSnapshot(
1215
+ db: Database,
1216
+ input: {
1217
+ accountId: string;
1218
+ workspaceId: string;
1219
+ subjectId: string;
1220
+ source: IntegrationSource;
1221
+ connectionId?: string;
1222
+ instanceKey?: string;
1223
+ expectedRevisionId: string;
1224
+ expectedContentSha256: string;
1225
+ allowedTools?: string[];
1226
+ },
1227
+ ): Promise<{
1228
+ runtime: ApiIntegrationRuntime;
1229
+ baseServerId: string;
1230
+ name: string;
1231
+ provider: string | null;
1232
+ requiredScopes: string[];
1233
+ } | null> {
1234
+ return withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (tx) => {
1235
+ const instanceKey = integrationBindingKey(input.connectionId, input.instanceKey);
1236
+ const candidates = (
1237
+ await listInstalledApiIntegrationsInRlsContext(tx, input.workspaceId, input.subjectId)
1238
+ ).filter((runtime) => {
1239
+ if (
1240
+ runtime.instanceKey !== instanceKey ||
1241
+ (runtime.connectionRef?.connectionId ?? null) !== (input.connectionId ?? null) ||
1242
+ runtime.revision.id !== input.expectedRevisionId ||
1243
+ runtime.revision.contentSha256 !== input.expectedContentSha256 ||
1244
+ !(input.allowedTools ?? runtime.revision.tools.map((tool) => tool.id)).every((tool) =>
1245
+ runtime.allowedTools.includes(tool),
1246
+ )
1247
+ )
1248
+ return false;
1249
+ const source = input.source;
1250
+ if (source.kind === "definition")
1251
+ return (
1252
+ runtime.definitionProvenance === "curated" && runtime.definitionId === source.definitionId
1253
+ );
1254
+ if (runtime.definitionProvenance !== "workspace") return false;
1255
+ if (source.kind === "graphql")
1256
+ return runtime.protocol === "graphql" && source.endpoint === runtime.sourceUrl;
1257
+ return (
1258
+ (source.kind === "auto" || runtime.protocol === "openapi") &&
1259
+ source.url === runtime.sourceUrl &&
1260
+ (source.baseUrl === undefined || source.baseUrl === runtime.baseUrl)
1261
+ );
1262
+ });
1263
+ if (candidates.length !== 1) return null;
1264
+ const runtime = candidates[0]!;
1265
+ const owners = await listIntegrationFacetBindingOwners(tx, runtime.instanceId);
1266
+ if (
1267
+ !owners.some(
1268
+ (owner) => owner.kind === "direct" && owner.id === runtime.capabilityId && owner.removable,
1269
+ )
1270
+ )
1271
+ return null;
1272
+ const [stored] = await tx
1273
+ .select({
1274
+ manifest: schema.capabilityPluginVersions.manifest,
1275
+ name: schema.capabilityPlugins.name,
1276
+ requiredScopes: schema.capabilityIntegrationFacets.requiredScopes,
1277
+ })
1278
+ .from(schema.capabilityPluginInstallations)
1279
+ .innerJoin(
1280
+ schema.capabilityPlugins,
1281
+ eq(schema.capabilityPlugins.id, schema.capabilityPluginInstallations.pluginId),
1282
+ )
1283
+ .innerJoin(
1284
+ schema.capabilityPluginVersions,
1285
+ eq(
1286
+ schema.capabilityPluginVersions.id,
1287
+ schema.capabilityPluginInstallations.pluginVersionId,
1288
+ ),
1289
+ )
1290
+ .innerJoin(
1291
+ schema.integrationFacetBindings,
1292
+ eq(schema.integrationFacetBindings.id, runtime.instanceId),
1293
+ )
1294
+ .innerJoin(
1295
+ schema.integrationFacetDefinitions,
1296
+ eq(
1297
+ schema.integrationFacetDefinitions.id,
1298
+ schema.integrationFacetBindings.facetDefinitionId,
1299
+ ),
1300
+ )
1301
+ .innerJoin(
1302
+ schema.capabilityIntegrationFacets,
1303
+ eq(
1304
+ schema.capabilityIntegrationFacets.facetId,
1305
+ schema.integrationFacetDefinitions.integrationFacetId,
1306
+ ),
1307
+ )
1308
+ .where(
1309
+ and(
1310
+ eq(schema.capabilityPluginInstallations.id, runtime.pluginInstallationId),
1311
+ eq(schema.capabilityPluginInstallations.accountId, input.accountId),
1312
+ eq(schema.capabilityPluginInstallations.workspaceId, input.workspaceId),
1313
+ eq(schema.capabilityPluginInstallations.status, "active"),
1314
+ ),
1315
+ )
1316
+ .limit(1);
1317
+ const manifest = objectValue(stored?.manifest);
1318
+ const baseServerId = stringValue(manifest.serverId);
1319
+ if (
1320
+ !stored ||
1321
+ !baseServerId ||
1322
+ manifest.revisionId !== runtime.revision.id ||
1323
+ manifest.contentSha256 !== runtime.revision.contentSha256 ||
1324
+ manifest.capabilityId !== runtime.capabilityId
1325
+ )
1326
+ return null;
1327
+ return {
1328
+ runtime,
1329
+ baseServerId,
1330
+ name: stored.name,
1331
+ provider: stringValue(manifest.provider) ?? null,
1332
+ requiredScopes: stored.requiredScopes,
1333
+ };
1334
+ });
1335
+ }
1336
+
1081
1337
  export async function uninstallApiIntegration(
1082
1338
  db: Database,
1083
1339
  input: {
@@ -1631,12 +1887,7 @@ async function integrationOwners(
1631
1887
  );
1632
1888
  const owners = new Map<string, ApiIntegrationOwner>();
1633
1889
  for (const row of rows) {
1634
- if (
1635
- row.kind !== "direct" &&
1636
- row.kind !== "plugin" &&
1637
- row.kind !== "pack" &&
1638
- row.kind !== "migration"
1639
- ) {
1890
+ if (row.kind !== "direct" && row.kind !== "plugin" && row.kind !== "migration") {
1640
1891
  throw new Error(`Unknown API Integration owner kind: ${row.kind}`);
1641
1892
  }
1642
1893
  const key = `${row.kind}\0${row.id}`;
@@ -1703,6 +1954,35 @@ function selectedToolIds(input: InstallApiIntegrationInput): string[] {
1703
1954
  return selected;
1704
1955
  }
1705
1956
 
1957
+ function isApiIntegrationPermissionReduction(
1958
+ previous: Record<string, unknown>,
1959
+ next: Record<string, unknown>,
1960
+ ): boolean {
1961
+ const {
1962
+ allowedTools: previousTools,
1963
+ requireApproval: previousApproval,
1964
+ ...previousIdentity
1965
+ } = previous;
1966
+ const { allowedTools: nextTools, requireApproval: nextApproval, ...nextIdentity } = next;
1967
+ const before = stringArray(previousTools);
1968
+ const after = stringArray(nextTools);
1969
+ const requiredBefore = stringArray(previousApproval);
1970
+ const requiredAfter = stringArray(nextApproval);
1971
+ return (
1972
+ before !== undefined &&
1973
+ after !== undefined &&
1974
+ requiredAfter !== undefined &&
1975
+ (previousApproval === true || requiredBefore !== undefined) &&
1976
+ stableJson(previousIdentity) === stableJson(nextIdentity) &&
1977
+ after.every(
1978
+ (tool) =>
1979
+ before.includes(tool) &&
1980
+ (!(previousApproval === true || requiredBefore?.includes(tool)) ||
1981
+ requiredAfter.includes(tool)),
1982
+ )
1983
+ );
1984
+ }
1985
+
1706
1986
  function storedRevision(value: Record<string, unknown>): StoredApiIntegrationRevision {
1707
1987
  const protocol = value.protocol;
1708
1988
  const tools = value.tools;
@@ -0,0 +1,19 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { withWorkspaceRls, type Database } from "./database";
3
+
4
+ /** Retain the installation row through the caller's outer transaction. This is
5
+ * a no-write reconciliation read; it never activates or alters an installation.
6
+ * Acquisition callers must already hold the organization policy fence. */
7
+ export async function withLockedCapabilityInstallation<T>(
8
+ db: Database,
9
+ workspaceId: string,
10
+ capabilityId: string,
11
+ read: (tx: Database) => Promise<T>,
12
+ ) {
13
+ return withWorkspaceRls(db, workspaceId, async (tx) => {
14
+ await tx.execute(sql`select id from capability_installations
15
+ where workspace_id = ${workspaceId}::uuid and capability_id = ${capabilityId}
16
+ for update`);
17
+ return read(tx);
18
+ });
19
+ }
@@ -0,0 +1,47 @@
1
+ /** Durable logical-turn state; checks and quota/account signals never spend this budget. */
2
+ export const CODEX_CAPACITY_RECOVERY_KEY = "codexCapacityRecoveryV1";
3
+ export const CODEX_CAPACITY_FALSE_RESUMPTION_LIMIT = 10;
4
+
5
+ export type CodexCapacityRecovery = {
6
+ falseResumptions: number;
7
+ resumeGeneration: number | null;
8
+ retryNotBefore: string | null;
9
+ };
10
+
11
+ export function readCodexCapacityRecovery(
12
+ metadata: Record<string, unknown> | null | undefined,
13
+ ): CodexCapacityRecovery {
14
+ const value = metadata?.[CODEX_CAPACITY_RECOVERY_KEY];
15
+ if (value === undefined) {
16
+ return { falseResumptions: 0, resumeGeneration: null, retryNotBefore: null };
17
+ }
18
+ const state = value as CodexCapacityRecovery | null;
19
+ if (
20
+ !state ||
21
+ !Number.isSafeInteger(state.falseResumptions) ||
22
+ state.falseResumptions < 0 ||
23
+ state.falseResumptions > CODEX_CAPACITY_FALSE_RESUMPTION_LIMIT ||
24
+ (state.resumeGeneration !== null &&
25
+ (!Number.isSafeInteger(state.resumeGeneration) || state.resumeGeneration < 1)) ||
26
+ (state.retryNotBefore !== null &&
27
+ (typeof state.retryNotBefore !== "string" ||
28
+ !Number.isFinite(Date.parse(state.retryNotBefore))))
29
+ ) {
30
+ throw new Error("Invalid persisted Codex capacity recovery state");
31
+ }
32
+ return state;
33
+ }
34
+
35
+ /** Equal jitter, 30–60 seconds initially, capped at 15 minutes. Persist the chosen deadline. */
36
+ export function codexFalseResumptionBackoffMs(count: number, random = Math.random()): number {
37
+ const cap = Math.min(60_000 * 2 ** Math.max(0, Math.min(count - 1, 4)), 900_000);
38
+ return Math.floor(cap * (0.5 + Math.max(0, Math.min(random, 1)) / 2));
39
+ }
40
+
41
+ export function clearCodexCapacityRecovery(
42
+ metadata: Record<string, unknown>,
43
+ ): Record<string, unknown> {
44
+ const next = { ...metadata };
45
+ delete next[CODEX_CAPACITY_RECOVERY_KEY];
46
+ return next;
47
+ }
@@ -0,0 +1,93 @@
1
+ /** Refusal-specific, ordered recovery evidence; never erase the failure ledger. */
2
+ export function unresolvedCodexCredentialFailures(
3
+ metadata: Record<string, unknown> | null | undefined,
4
+ accounts: readonly {
5
+ id: string;
6
+ status: string;
7
+ exhaustedUntil: Date | null;
8
+ exhaustedKind: string | null;
9
+ exhaustedRevision?: number;
10
+ credentialVersion?: number;
11
+ }[],
12
+ now = new Date(),
13
+ ): string[] {
14
+ const failedIds = Array.isArray(metadata?.codexCredentialFailedIds)
15
+ ? metadata.codexCredentialFailedIds.filter(
16
+ (id): id is string => typeof id === "string" && id.length > 0,
17
+ )
18
+ : [];
19
+ const rawRevisions = metadata?.codexCredentialFailureCooldownRevisions;
20
+ const revisions =
21
+ rawRevisions && typeof rawRevisions === "object" && !Array.isArray(rawRevisions)
22
+ ? (rawRevisions as Record<string, unknown>)
23
+ : null;
24
+ const rawEvidence = metadata?.codexCredentialFailureEvidenceV1;
25
+ const evidence =
26
+ rawEvidence && typeof rawEvidence === "object" && !Array.isArray(rawEvidence)
27
+ ? (rawEvidence as Record<string, unknown>)
28
+ : null;
29
+ return [...new Set(failedIds)].filter((id) => {
30
+ const account = accounts.find((candidate) => candidate.id === id);
31
+ const revision = account?.exhaustedRevision;
32
+ const baseline = revisions?.[id];
33
+ const rawReceipt = evidence?.[id];
34
+ const receipt =
35
+ rawReceipt && typeof rawReceipt === "object" && !Array.isArray(rawReceipt)
36
+ ? (rawReceipt as Record<string, unknown>)
37
+ : null;
38
+ if (
39
+ rawReceipt !== undefined &&
40
+ (!receipt || !["quota", "rate_limit", "status"].includes(String(receipt.kind)))
41
+ )
42
+ return true;
43
+ // The earlier atomic quarantine writer used explicit null only for status
44
+ // refusals (it wrote needs_relogin/error in the same transaction). Seeing
45
+ // active again therefore proves a later repair. Missing ID-only evidence
46
+ // is different and remains excluded. New receipts additionally fence the
47
+ // repaired credential version below.
48
+ if (!receipt && baseline === null && account?.status === "active") return false;
49
+ if (account?.status === "active" && receipt?.kind === "status") {
50
+ return !(
51
+ typeof receipt.credentialVersion === "number" &&
52
+ Number.isSafeInteger(receipt.credentialVersion) &&
53
+ receipt.credentialVersion >= 1 &&
54
+ typeof account.credentialVersion === "number" &&
55
+ Number.isSafeInteger(account.credentialVersion) &&
56
+ account.credentialVersion > receipt.credentialVersion
57
+ );
58
+ }
59
+ if (
60
+ account?.status === "active" &&
61
+ ((receipt?.kind === "rate_limit" &&
62
+ typeof receipt.cooldownRevision === "number" &&
63
+ Number.isSafeInteger(receipt.cooldownRevision) &&
64
+ receipt.cooldownRevision >= 1 &&
65
+ typeof revision === "number" &&
66
+ revision >= receipt.cooldownRevision) ||
67
+ (!receipt &&
68
+ typeof baseline === "number" &&
69
+ Number.isSafeInteger(baseline) &&
70
+ baseline >= 1 &&
71
+ revision === baseline)) &&
72
+ typeof revision === "number" &&
73
+ Number.isSafeInteger(revision) &&
74
+ account.exhaustedKind === "rate_limit" &&
75
+ account.exhaustedUntil !== null &&
76
+ account.exhaustedUntil.getTime() <= now.getTime()
77
+ ) {
78
+ return false;
79
+ }
80
+ // Quota recovery needs an ordered explicit clear, including older numeric receipts.
81
+ return !(
82
+ account?.status === "active" &&
83
+ account.exhaustedUntil === null &&
84
+ account.exhaustedKind === null &&
85
+ typeof baseline === "number" &&
86
+ Number.isSafeInteger(baseline) &&
87
+ baseline >= 1 &&
88
+ typeof revision === "number" &&
89
+ Number.isSafeInteger(revision) &&
90
+ revision > baseline
91
+ );
92
+ });
93
+ }
@@ -98,6 +98,45 @@ function validateOperation(id: string, digest: string): void {
98
98
 
99
99
  /** Caller must establish live workspace/actor authority before this storage
100
100
  * seam. RLS scope is a consistency boundary, not proof of authentication. */
101
+ type ConnectBeginIdentity = {
102
+ idempotencyKey: string;
103
+ requestDigest: string;
104
+ returnUrl: string;
105
+ externalContinuation?: ExternalActorContinuation;
106
+ };
107
+
108
+ /** Recover only an exact saved begin request, under the caller's live actor
109
+ * admission. This performs no provider work or acquisition. */
110
+ export async function getConnectBeginReplay(
111
+ db: Database,
112
+ scope: ConnectActorScope,
113
+ input: ConnectBeginIdentity,
114
+ ): Promise<Attempt | null> {
115
+ validateOperation(input.idempotencyKey, input.requestDigest);
116
+ const origin = input.externalContinuation
117
+ ? ExternalActorContinuation.parse(input.externalContinuation)
118
+ : null;
119
+ const key = createHash("sha256").update(input.idempotencyKey).digest("hex");
120
+ return scoped(db, scope, async (tx) => {
121
+ const [existing] = await rawRows<Row>(
122
+ tx,
123
+ sql`select * from connect_attempts
124
+ where workspace_id = ${scope.workspaceId}::uuid and subject_id = ${scope.subjectId}
125
+ and idempotency_key_hash = ${key} and account_id = ${scope.accountId}::uuid`,
126
+ );
127
+ if (!existing) return null;
128
+ if (
129
+ existing.request_digest !== input.requestDigest ||
130
+ existing.return_url !== input.returnUrl ||
131
+ stableJson(existing.external_continuation ?? null) !== stableJson(origin)
132
+ )
133
+ throw new ConnectAttemptConflictError(
134
+ "Connect idempotency key was reused with different input",
135
+ );
136
+ return projection(existing, scope);
137
+ });
138
+ }
139
+
101
140
  export async function beginConnectAttempt(
102
141
  db: Database,
103
142
  scope: ConnectActorScope,
@@ -107,6 +146,7 @@ export async function beginConnectAttempt(
107
146
  returnUrl: string;
108
147
  attempt: Attempt;
109
148
  externalContinuation?: ExternalActorContinuation;
149
+ authorizeAcquisition?: (tx: Database) => Promise<void>;
110
150
  },
111
151
  ): Promise<Attempt> {
112
152
  const value = ConnectAttempt.parse(input.attempt);
@@ -128,24 +168,9 @@ export async function beginConnectAttempt(
128
168
  and expires_at < now() - interval '30 days'
129
169
  order by expires_at, id limit 100 for update skip locked
130
170
  ) delete from connect_attempts where id in (select id from expired)`);
131
- const [existing] = await rawRows<Row>(
132
- tx,
133
- sql`select * from connect_attempts
134
- where workspace_id = ${scope.workspaceId}::uuid and subject_id = ${scope.subjectId}
135
- and idempotency_key_hash = ${key} and account_id = ${scope.accountId}::uuid`,
136
- );
137
- if (existing) {
138
- if (
139
- existing.request_digest !== input.requestDigest ||
140
- existing.return_url !== input.returnUrl ||
141
- stableJson(existing.external_continuation ?? null) !== stableJson(origin)
142
- ) {
143
- throw new ConnectAttemptConflictError(
144
- "Connect idempotency key was reused with different input",
145
- );
146
- }
147
- return projection(existing, scope);
148
- }
171
+ const existing = await getConnectBeginReplay(tx, scope, input);
172
+ if (existing) return existing;
173
+ await input.authorizeAcquisition?.(tx);
149
174
  const [count] = await rawRows<{ count: number }>(
150
175
  tx,
151
176
  sql`select count(*)::int as count from connect_attempts
@@ -227,6 +252,8 @@ export async function claimConnectOperation(
227
252
  operationId: string;
228
253
  inputDigest: string;
229
254
  authorize?: ConnectOperationAuthorization;
255
+ /** Additional acquisition admission, only after exact no-effect receipt replay. */
256
+ authorizeAcquisition?: (tx: Database, attempt: Attempt) => Promise<void>;
230
257
  },
231
258
  ): Promise<{ status: "claimed"; attempt: Attempt } | { status: "replayed"; attempt: Attempt }> {
232
259
  validateOperation(input.operationId, input.inputDigest);
@@ -242,6 +269,7 @@ export async function claimConnectOperation(
242
269
  if (receipt) {
243
270
  return { status: "replayed", attempt: receipt };
244
271
  }
272
+ await input.authorizeAcquisition?.(tx, structuredClone(current));
245
273
  if (row.operation_id)
246
274
  throw new ConnectAttemptConflictError(
247
275
  "Connect operation is in flight or uncertain; reconcile before retrying",
@@ -272,6 +300,8 @@ export async function finishConnectOperation(
272
300
  inputDigest: string;
273
301
  commit: (tx: Database, current: Attempt) => Promise<Attempt>;
274
302
  authorize?: ConnectOperationAuthorization;
303
+ /** Additional acquisition admission, only after exact no-effect receipt replay. */
304
+ authorizeAcquisition?: (tx: Database, attempt: Attempt) => Promise<void>;
275
305
  },
276
306
  ): Promise<Attempt> {
277
307
  validateOperation(input.operationId, input.inputDigest);
@@ -287,6 +317,7 @@ export async function finishConnectOperation(
287
317
  if (receipt) {
288
318
  return receipt;
289
319
  }
320
+ await input.authorizeAcquisition?.(tx, structuredClone(current));
290
321
  if (row.operation_id !== input.operationId || row.operation_digest !== input.inputDigest)
291
322
  throw new ConnectAttemptConflictError();
292
323
  const next = ConnectAttempt.parse(await input.commit(tx, structuredClone(current)));