@opengeni/db 3.7.4 → 4.0.0-canary.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 (113) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-integrations.d.ts +2 -0
  4. package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
  5. package/dist/chunk-2XEZK4LF.js.map +1 -0
  6. package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
  7. package/dist/chunk-3RI33YQN.js.map +1 -0
  8. package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
  9. package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
  10. package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
  11. package/dist/chunk-N5VXJ4I2.js.map +1 -0
  12. package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
  13. package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
  14. package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
  15. package/dist/chunk-QM6YOWHZ.js.map +1 -0
  16. package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
  17. package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
  18. package/dist/codemode-operations.d.ts +5 -0
  19. package/dist/codex-token-resolver.d.ts +6 -2
  20. package/dist/connection-token-resolver.d.ts +8 -0
  21. package/dist/editable-artifact-durable-export.js +2 -2
  22. package/dist/editable-artifacts.js +3 -3
  23. package/dist/generated-images.d.ts +15 -0
  24. package/dist/index.d.ts +393 -91
  25. package/dist/index.js +9992 -6069
  26. package/dist/index.js.map +1 -1
  27. package/dist/insights.d.ts +9 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/managed-user-setup.d.ts +3 -0
  30. package/dist/mcp-oauth.d.ts +80 -0
  31. package/dist/migrate.d.ts +1 -0
  32. package/dist/migrate.js +3 -1
  33. package/dist/model-catalog.d.ts +153 -0
  34. package/dist/model-context-snapshots.d.ts +20 -0
  35. package/dist/new-session-drafts.d.ts +12 -0
  36. package/dist/organization-membership-lifecycle.d.ts +8 -1
  37. package/dist/organization-model-providers.d.ts +101 -0
  38. package/dist/pack-components.d.ts +1 -0
  39. package/dist/pr-review.d.ts +9 -0
  40. package/dist/provision-roles.js +1 -1
  41. package/dist/runtime-posture.d.ts +8 -8
  42. package/dist/schema.d.ts +7708 -5050
  43. package/dist/schema.js +25 -1
  44. package/dist/session-background-commands.d.ts +18 -6
  45. package/dist/session-background-commands.js +15 -9
  46. package/dist/session-control.d.ts +37 -2
  47. package/dist/session-goal-pacing.d.ts +3 -6
  48. package/dist/session-queue-commands.d.ts +17 -0
  49. package/dist/session-tenancy.js +3 -3
  50. package/dist/tool-gateway-approvals.d.ts +31 -0
  51. package/dist/video-generation.js +3 -3
  52. package/dist/workspace-artifacts.d.ts +17 -3
  53. package/dist/xai-subscription.d.ts +8 -0
  54. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  55. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  56. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  57. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  58. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  59. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  60. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  61. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  62. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  63. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  64. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  65. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  66. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  67. package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
  68. package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
  69. package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
  70. package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
  71. package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
  72. package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
  73. package/drizzle/0406_workspace_html_site_sources.sql +71 -0
  74. package/package.json +6 -6
  75. package/src/automations.ts +9 -1
  76. package/src/capability-integrations.ts +8 -0
  77. package/src/codemode-operations.ts +31 -21
  78. package/src/codex-token-resolver.ts +16 -6
  79. package/src/connection-token-resolver.ts +52 -3
  80. package/src/generated-images.ts +70 -6
  81. package/src/index.ts +3894 -994
  82. package/src/insights-model-bundle.ts +54 -5
  83. package/src/insights.ts +53 -7
  84. package/src/managed-user-setup.ts +14 -3
  85. package/src/mcp-oauth.ts +488 -0
  86. package/src/migrate.ts +9 -1
  87. package/src/model-catalog.ts +682 -0
  88. package/src/model-context-snapshots.ts +151 -0
  89. package/src/new-session-drafts.ts +66 -21
  90. package/src/organization-membership-lifecycle.ts +47 -5
  91. package/src/organization-model-providers.ts +614 -0
  92. package/src/pack-components.ts +15 -2
  93. package/src/pr-review.ts +19 -0
  94. package/src/provision-roles.ts +4 -0
  95. package/src/runtime-posture.ts +64 -0
  96. package/src/schema.ts +666 -61
  97. package/src/session-background-commands.ts +180 -78
  98. package/src/session-control.ts +181 -12
  99. package/src/session-goal-pacing.ts +4 -8
  100. package/src/session-queue-commands.ts +157 -82
  101. package/src/tool-gateway-approvals.ts +168 -0
  102. package/src/workspace-artifacts.ts +633 -178
  103. package/src/xai-subscription.ts +66 -14
  104. package/dist/chunk-4NAR7FBA.js.map +0 -1
  105. package/dist/chunk-6DPJA36C.js.map +0 -1
  106. package/dist/chunk-O56DM5GZ.js.map +0 -1
  107. package/dist/chunk-S3TDDCS2.js.map +0 -1
  108. /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
  109. /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
  110. /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
  111. /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
  112. /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
  113. /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
@@ -44,10 +44,13 @@ import {
44
44
  canonicalSessionCommandHash,
45
45
  evaluateSessionControl,
46
46
  lockSessionEventWriteRows,
47
+ lockSessionPromptCommandOperation,
47
48
  lockWorkspaceInferenceControl,
48
49
  lockWorkspaceInferenceControlForAdmission,
50
+ getCompletedSessionPromptCommandReceipt,
49
51
  registerInternalUpdateWakeInTransaction,
50
52
  reserveSessionCommandReceipt,
53
+ reserveSessionPromptCommandReceipt,
51
54
  registerSessionWorkflowWakeInTransaction,
52
55
  type SessionCommandActor,
53
56
  type SessionCommandReceiptRow,
@@ -210,6 +213,122 @@ function mapSubmittedPromptTurn(row: typeof schema.sessionTurns.$inferSelect): S
210
213
  };
211
214
  }
212
215
 
216
+ async function loadSubmittedHumanPromptReplay(
217
+ db: Database,
218
+ input: {
219
+ workspaceId: string;
220
+ sessionId: string;
221
+ subjectId: string;
222
+ delivery: "send" | "steer";
223
+ expectedDraftRevision?: number | null;
224
+ receipt: SessionCommandReceiptRow;
225
+ },
226
+ ): Promise<SubmitHumanPromptResult> {
227
+ const turnId = String(input.receipt.result.turnId ?? "");
228
+ const acceptedEventId = String(input.receipt.result.acceptedEventId ?? "");
229
+ const eventIds = Array.isArray(input.receipt.result.eventIds)
230
+ ? input.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
231
+ : [];
232
+ const wakeRevision = Number(input.receipt.result.wakeRevision ?? 0);
233
+ if (!turnId || !acceptedEventId || wakeRevision < 1) {
234
+ throw new SessionControlInvariantError("Replayed prompt receipt is incomplete");
235
+ }
236
+ const replayEvents = await db
237
+ .select()
238
+ .from(schema.sessionEvents)
239
+ .where(
240
+ and(
241
+ eq(schema.sessionEvents.workspaceId, input.workspaceId),
242
+ eq(schema.sessionEvents.sessionId, input.sessionId),
243
+ inArray(schema.sessionEvents.id, eventIds),
244
+ ),
245
+ )
246
+ .orderBy(asc(schema.sessionEvents.sequence));
247
+ const [replayTurn] = await db
248
+ .select()
249
+ .from(schema.sessionTurns)
250
+ .where(
251
+ and(
252
+ eq(schema.sessionTurns.workspaceId, input.workspaceId),
253
+ eq(schema.sessionTurns.sessionId, input.sessionId),
254
+ eq(schema.sessionTurns.id, turnId),
255
+ ),
256
+ )
257
+ .limit(1);
258
+ const events = replayEvents.map(mapSubmittedPromptEvent);
259
+ const accepted = events.find((event) => event.id === acceptedEventId);
260
+ if (!accepted || !replayTurn || events.length !== eventIds.length) {
261
+ throw new SessionControlInvariantError("Replayed prompt rows are incomplete");
262
+ }
263
+ const replayDraft =
264
+ input.expectedDraftRevision === null || input.expectedDraftRevision === undefined
265
+ ? null
266
+ : await getComposerDraftInTransaction(db, {
267
+ workspaceId: input.workspaceId,
268
+ sessionId: input.sessionId,
269
+ subjectId: input.subjectId,
270
+ });
271
+ return {
272
+ receipt: input.receipt,
273
+ routing:
274
+ input.receipt.result.routing === "accepted_for_execution" ||
275
+ input.receipt.result.routing === "queued_for_execution" ||
276
+ input.receipt.result.routing === "accepted_for_steering"
277
+ ? input.receipt.result.routing
278
+ : input.delivery === "steer"
279
+ ? "accepted_for_steering"
280
+ : // Pre-routing receipts cannot reconstruct admission-time activity
281
+ // or Pause truth. Queue is the only conservative placement; a
282
+ // later turn.started event moves it into chat without duplication.
283
+ "queued_for_execution",
284
+ queueVersion: Number(input.receipt.appliedQueueVersion),
285
+ accepted,
286
+ events,
287
+ turn: mapSubmittedPromptTurn(replayTurn),
288
+ draft: replayDraft,
289
+ acceptedEventId,
290
+ eventIds,
291
+ turnId,
292
+ wakeRevision,
293
+ interruptionCount: Number(input.receipt.result.interruptionCount ?? 0),
294
+ workspaceControlEventId:
295
+ typeof input.receipt.result.workspaceControlEventId === "string"
296
+ ? input.receipt.result.workspaceControlEventId
297
+ : null,
298
+ replay: true,
299
+ };
300
+ }
301
+
302
+ export async function replaySubmittedHumanPromptFromBoundaryReceipt(
303
+ db: Database,
304
+ input: {
305
+ workspaceId: string;
306
+ sessionId: string;
307
+ subjectId: string;
308
+ actor: SessionCommandActor;
309
+ operationKey: string;
310
+ delivery: "send" | "steer";
311
+ boundaryRequestHash: string;
312
+ expectedDraftRevision?: number | null;
313
+ /** Wait for an overlapping same-key transaction before checking committed replay truth. */
314
+ serializeOperation?: boolean;
315
+ },
316
+ ): Promise<SubmitHumanPromptResult | null> {
317
+ if (input.serializeOperation) {
318
+ await lockSessionPromptCommandOperation(db, input);
319
+ }
320
+ const receipt = await getCompletedSessionPromptCommandReceipt(db, {
321
+ workspaceId: input.workspaceId,
322
+ actor: input.actor,
323
+ action: input.delivery === "steer" ? "prompt.steer" : "prompt.send",
324
+ targetSessionId: input.sessionId,
325
+ operationKey: input.operationKey,
326
+ boundaryRequestHash: input.boundaryRequestHash,
327
+ });
328
+ if (!receipt) return null;
329
+ return await loadSubmittedHumanPromptReplay(db, { ...input, receipt });
330
+ }
331
+
213
332
  export type AgentInternalUpdateCommandResult = {
214
333
  receipt: SessionCommandReceiptRow;
215
334
  updateId: string;
@@ -1608,6 +1727,8 @@ export async function submitHumanPromptInTransaction(
1608
1727
  subjectLabel?: string;
1609
1728
  actor: SessionCommandActor;
1610
1729
  operationKey: string;
1730
+ /** Stable parsed boundary-request identity used before mutable enrichment. */
1731
+ boundaryRequestHash?: string;
1611
1732
  delivery: "send" | "steer";
1612
1733
  controlEtag?: string | null;
1613
1734
  expectedDraftRevision?: number | null;
@@ -1642,6 +1763,9 @@ export async function submitHumanPromptInTransaction(
1642
1763
  id: string;
1643
1764
  headersEncrypted: Record<string, string>;
1644
1765
  }>;
1766
+ /** Trusted database-only admission seam. It runs only after a completed
1767
+ * operation replay has been ruled out; throwing rolls the prompt back. */
1768
+ beforeFreshPromptCommit?: (tx: Database) => Promise<void>;
1645
1769
  /** Request-scoped callers bound the control prefix wait; lifecycle callers omit it. */
1646
1770
  controlLockTimeoutMs?: number;
1647
1771
  },
@@ -1708,91 +1832,41 @@ export async function submitHumanPromptInTransaction(
1708
1832
  }
1709
1833
  : {}),
1710
1834
  });
1711
- const reserved = await reserveSessionCommandReceipt(db, {
1712
- accountId: input.accountId,
1713
- workspaceId: input.workspaceId,
1714
- actor: input.actor,
1715
- action: input.delivery === "steer" ? "prompt.steer" : "prompt.send",
1716
- targetSessionId: input.sessionId,
1717
- targetTurnId: null,
1718
- operationKey: input.operationKey,
1719
- canonicalRequestHash: requestHash,
1720
- });
1835
+ const action = input.delivery === "steer" ? "prompt.steer" : "prompt.send";
1836
+ const reserved = input.boundaryRequestHash
1837
+ ? await reserveSessionPromptCommandReceipt(db, {
1838
+ accountId: input.accountId,
1839
+ workspaceId: input.workspaceId,
1840
+ actor: input.actor,
1841
+ action,
1842
+ targetSessionId: input.sessionId,
1843
+ operationKey: input.operationKey,
1844
+ canonicalRequestHash: requestHash,
1845
+ boundaryRequestHash: input.boundaryRequestHash,
1846
+ })
1847
+ : await reserveSessionCommandReceipt(db, {
1848
+ accountId: input.accountId,
1849
+ workspaceId: input.workspaceId,
1850
+ actor: input.actor,
1851
+ action,
1852
+ targetSessionId: input.sessionId,
1853
+ targetTurnId: null,
1854
+ operationKey: input.operationKey,
1855
+ canonicalRequestHash: requestHash,
1856
+ });
1721
1857
  if (reserved.replay && reserved.receipt.appliedQueueVersion !== null) {
1722
- const turnId = String(reserved.receipt.result.turnId ?? "");
1723
- const acceptedEventId = String(reserved.receipt.result.acceptedEventId ?? "");
1724
- const eventIds = Array.isArray(reserved.receipt.result.eventIds)
1725
- ? reserved.receipt.result.eventIds.filter((id): id is string => typeof id === "string")
1726
- : [];
1727
- const wakeRevision = Number(reserved.receipt.result.wakeRevision ?? 0);
1728
- if (!turnId || !acceptedEventId || wakeRevision < 1) {
1729
- throw new SessionControlInvariantError("Replayed prompt receipt is incomplete");
1730
- }
1731
- const replayEvents = await db
1732
- .select()
1733
- .from(schema.sessionEvents)
1734
- .where(
1735
- and(
1736
- eq(schema.sessionEvents.workspaceId, input.workspaceId),
1737
- eq(schema.sessionEvents.sessionId, input.sessionId),
1738
- inArray(schema.sessionEvents.id, eventIds),
1739
- ),
1740
- )
1741
- .orderBy(asc(schema.sessionEvents.sequence));
1742
- const [replayTurn] = await db
1743
- .select()
1744
- .from(schema.sessionTurns)
1745
- .where(
1746
- and(
1747
- eq(schema.sessionTurns.workspaceId, input.workspaceId),
1748
- eq(schema.sessionTurns.sessionId, input.sessionId),
1749
- eq(schema.sessionTurns.id, turnId),
1750
- ),
1751
- )
1752
- .limit(1);
1753
- const events = replayEvents.map(mapSubmittedPromptEvent);
1754
- const accepted = events.find((event) => event.id === acceptedEventId);
1755
- if (!accepted || !replayTurn || events.length !== eventIds.length) {
1756
- throw new SessionControlInvariantError("Replayed prompt rows are incomplete");
1757
- }
1758
- const replayDraft =
1759
- input.expectedDraftRevision === null || input.expectedDraftRevision === undefined
1760
- ? null
1761
- : await getComposerDraftInTransaction(db, {
1762
- workspaceId: input.workspaceId,
1763
- sessionId: input.sessionId,
1764
- subjectId: input.subjectId,
1765
- });
1766
- return {
1858
+ return await loadSubmittedHumanPromptReplay(db, {
1859
+ workspaceId: input.workspaceId,
1860
+ sessionId: input.sessionId,
1861
+ subjectId: input.subjectId,
1862
+ delivery: input.delivery,
1863
+ ...(input.expectedDraftRevision !== undefined
1864
+ ? { expectedDraftRevision: input.expectedDraftRevision }
1865
+ : {}),
1767
1866
  receipt: reserved.receipt,
1768
- routing:
1769
- reserved.receipt.result.routing === "accepted_for_execution" ||
1770
- reserved.receipt.result.routing === "queued_for_execution" ||
1771
- reserved.receipt.result.routing === "accepted_for_steering"
1772
- ? reserved.receipt.result.routing
1773
- : input.delivery === "steer"
1774
- ? "accepted_for_steering"
1775
- : // Pre-routing receipts cannot reconstruct admission-time activity
1776
- // or Pause truth. Queue is the only conservative placement; a
1777
- // later turn.started event moves it into chat without duplication.
1778
- "queued_for_execution",
1779
- queueVersion: Number(reserved.receipt.appliedQueueVersion),
1780
- accepted,
1781
- events,
1782
- turn: mapSubmittedPromptTurn(replayTurn),
1783
- draft: replayDraft,
1784
- acceptedEventId,
1785
- eventIds,
1786
- turnId,
1787
- wakeRevision,
1788
- interruptionCount: Number(reserved.receipt.result.interruptionCount ?? 0),
1789
- workspaceControlEventId:
1790
- typeof reserved.receipt.result.workspaceControlEventId === "string"
1791
- ? reserved.receipt.result.workspaceControlEventId
1792
- : null,
1793
- replay: true,
1794
- };
1867
+ });
1795
1868
  }
1869
+ await input.beforeFreshPromptCommit?.(db);
1796
1870
 
1797
1871
  const before = await evaluateSessionControl(db, input.workspaceId, input.sessionId, {
1798
1872
  workspaceControl,
@@ -2434,6 +2508,7 @@ export async function submitHumanPromptInTransaction(
2434
2508
  turnVersion: turn.version,
2435
2509
  ...(nextDraft ? { draftRevision: nextDraft.revision } : {}),
2436
2510
  result: {
2511
+ ...(input.boundaryRequestHash ? { boundaryRequestHash: input.boundaryRequestHash } : {}),
2437
2512
  turnId,
2438
2513
  acceptedEventId,
2439
2514
  eventIds,
@@ -0,0 +1,168 @@
1
+ import { and, eq, gt, isNull, sql } from "drizzle-orm";
2
+ import type { ToolGatewayIdentity } from "@opengeni/contracts";
3
+ import type { Database } from "./database";
4
+ import { withWorkspaceSubjectRls } from "./database";
5
+ import * as schema from "./schema";
6
+
7
+ const MAX_LIVE_APPROVALS_PER_SUBJECT = 100;
8
+
9
+ export class ToolGatewayApprovalRateLimitError extends Error {
10
+ readonly name = "ToolGatewayApprovalRateLimitError";
11
+ }
12
+
13
+ export class ToolGatewayApprovalOperationStartedError extends Error {
14
+ readonly name = "ToolGatewayApprovalOperationStartedError";
15
+ }
16
+
17
+ export async function issueToolGatewayApproval(
18
+ db: Database,
19
+ input: {
20
+ tokenHash: string;
21
+ accountId: string;
22
+ workspaceId: string;
23
+ subjectId: string;
24
+ operationId: string;
25
+ catalogDigest: string;
26
+ identity: ToolGatewayIdentity;
27
+ argumentsDigest: string;
28
+ approvalAuthorityDigest: string;
29
+ expiresAt: Date;
30
+ },
31
+ ): Promise<void> {
32
+ await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
33
+ await lockApprovalSubject(scopedDb, input.workspaceId, input.subjectId);
34
+ await lockApprovalOperation(scopedDb, input.workspaceId, input.subjectId, input.operationId);
35
+ const [existing] = await scopedDb
36
+ .select({
37
+ tokenHash: schema.toolGatewayApprovalCapabilities.tokenHash,
38
+ consumedAt: schema.toolGatewayApprovalCapabilities.consumedAt,
39
+ })
40
+ .from(schema.toolGatewayApprovalCapabilities)
41
+ .where(
42
+ and(
43
+ eq(schema.toolGatewayApprovalCapabilities.workspaceId, input.workspaceId),
44
+ eq(schema.toolGatewayApprovalCapabilities.subjectId, input.subjectId),
45
+ eq(schema.toolGatewayApprovalCapabilities.operationId, input.operationId),
46
+ ),
47
+ )
48
+ .limit(1);
49
+ if (existing?.consumedAt) {
50
+ throw new ToolGatewayApprovalOperationStartedError(
51
+ "The tool operation has already consumed its approval",
52
+ );
53
+ }
54
+ if (existing) {
55
+ // Reapproval before execution is allowed when catalog or provider
56
+ // authority changed. Once consumed, the row is retained permanently as
57
+ // the operation tombstone above and is never replaced.
58
+ await scopedDb
59
+ .delete(schema.toolGatewayApprovalCapabilities)
60
+ .where(eq(schema.toolGatewayApprovalCapabilities.tokenHash, existing.tokenHash));
61
+ }
62
+ await scopedDb.execute(sql`
63
+ delete from tool_gateway_approval_capabilities
64
+ where token_hash in (
65
+ select token_hash
66
+ from tool_gateway_approval_capabilities
67
+ where workspace_id = ${input.workspaceId}
68
+ and subject_id = ${input.subjectId}
69
+ and expires_at <= clock_timestamp()
70
+ and consumed_at is null
71
+ order by expires_at, token_hash
72
+ limit 128
73
+ )
74
+ `);
75
+ const [count] = await scopedDb
76
+ .select({ value: sql<number>`count(*)::integer` })
77
+ .from(schema.toolGatewayApprovalCapabilities)
78
+ .where(
79
+ and(
80
+ eq(schema.toolGatewayApprovalCapabilities.workspaceId, input.workspaceId),
81
+ eq(schema.toolGatewayApprovalCapabilities.subjectId, input.subjectId),
82
+ isNull(schema.toolGatewayApprovalCapabilities.consumedAt),
83
+ gt(schema.toolGatewayApprovalCapabilities.expiresAt, new Date()),
84
+ ),
85
+ );
86
+ if ((count?.value ?? 0) >= MAX_LIVE_APPROVALS_PER_SUBJECT) {
87
+ throw new ToolGatewayApprovalRateLimitError("Too many live tool approvals");
88
+ }
89
+ await scopedDb.insert(schema.toolGatewayApprovalCapabilities).values({
90
+ tokenHash: input.tokenHash,
91
+ accountId: input.accountId,
92
+ workspaceId: input.workspaceId,
93
+ subjectId: input.subjectId,
94
+ operationId: input.operationId,
95
+ catalogDigest: input.catalogDigest,
96
+ serverId: input.identity.serverId,
97
+ toolName: input.identity.toolName,
98
+ argumentsDigest: input.argumentsDigest,
99
+ authorityDigest: input.approvalAuthorityDigest,
100
+ expiresAt: input.expiresAt,
101
+ });
102
+ });
103
+ }
104
+
105
+ export async function consumeToolGatewayApproval(
106
+ db: Database,
107
+ input: {
108
+ tokenHash: string;
109
+ accountId: string;
110
+ workspaceId: string;
111
+ subjectId: string;
112
+ operationId: string;
113
+ catalogDigest: string;
114
+ identity: ToolGatewayIdentity;
115
+ argumentsDigest: string;
116
+ approvalAuthorityDigest: string;
117
+ },
118
+ ): Promise<boolean> {
119
+ return await withWorkspaceSubjectRls(db, input.workspaceId, input.subjectId, async (scopedDb) => {
120
+ await lockApprovalOperation(scopedDb, input.workspaceId, input.subjectId, input.operationId);
121
+ const [consumed] = await scopedDb
122
+ .update(schema.toolGatewayApprovalCapabilities)
123
+ .set({ consumedAt: new Date() })
124
+ .where(
125
+ and(
126
+ eq(schema.toolGatewayApprovalCapabilities.tokenHash, input.tokenHash),
127
+ eq(schema.toolGatewayApprovalCapabilities.accountId, input.accountId),
128
+ eq(schema.toolGatewayApprovalCapabilities.workspaceId, input.workspaceId),
129
+ eq(schema.toolGatewayApprovalCapabilities.subjectId, input.subjectId),
130
+ eq(schema.toolGatewayApprovalCapabilities.operationId, input.operationId),
131
+ eq(schema.toolGatewayApprovalCapabilities.catalogDigest, input.catalogDigest),
132
+ eq(schema.toolGatewayApprovalCapabilities.serverId, input.identity.serverId),
133
+ eq(schema.toolGatewayApprovalCapabilities.toolName, input.identity.toolName),
134
+ eq(schema.toolGatewayApprovalCapabilities.argumentsDigest, input.argumentsDigest),
135
+ eq(schema.toolGatewayApprovalCapabilities.authorityDigest, input.approvalAuthorityDigest),
136
+ isNull(schema.toolGatewayApprovalCapabilities.consumedAt),
137
+ gt(schema.toolGatewayApprovalCapabilities.expiresAt, new Date()),
138
+ ),
139
+ )
140
+ .returning({ tokenHash: schema.toolGatewayApprovalCapabilities.tokenHash });
141
+ return consumed !== undefined;
142
+ });
143
+ }
144
+
145
+ async function lockApprovalSubject(
146
+ db: Database,
147
+ workspaceId: string,
148
+ subjectId: string,
149
+ ): Promise<void> {
150
+ await db.execute(
151
+ sql`select pg_advisory_xact_lock(
152
+ hashtextextended(${`tool-gateway-approval-subject:${workspaceId}:${subjectId}`}, 0)
153
+ )`,
154
+ );
155
+ }
156
+
157
+ async function lockApprovalOperation(
158
+ db: Database,
159
+ workspaceId: string,
160
+ subjectId: string,
161
+ operationId: string,
162
+ ): Promise<void> {
163
+ await db.execute(
164
+ sql`select pg_advisory_xact_lock(
165
+ hashtextextended(${`tool-gateway-approval:${workspaceId}:${subjectId}:${operationId}`}, 0)
166
+ )`,
167
+ );
168
+ }