@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.
- package/dist/automations.d.ts +4 -0
- package/dist/canonical-human-identities.js +3 -3
- package/dist/capability-integrations.d.ts +2 -0
- package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
- package/dist/chunk-2XEZK4LF.js.map +1 -0
- package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
- package/dist/chunk-3RI33YQN.js.map +1 -0
- package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
- package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
- package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
- package/dist/chunk-N5VXJ4I2.js.map +1 -0
- package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
- package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
- package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
- package/dist/chunk-QM6YOWHZ.js.map +1 -0
- package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
- package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
- package/dist/codemode-operations.d.ts +5 -0
- package/dist/codex-token-resolver.d.ts +6 -2
- package/dist/connection-token-resolver.d.ts +8 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/generated-images.d.ts +15 -0
- package/dist/index.d.ts +393 -91
- package/dist/index.js +9992 -6069
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +9 -0
- package/dist/managed-auth-session-sets.js +3 -3
- package/dist/managed-user-setup.d.ts +3 -0
- package/dist/mcp-oauth.d.ts +80 -0
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +3 -1
- package/dist/model-catalog.d.ts +153 -0
- package/dist/model-context-snapshots.d.ts +20 -0
- package/dist/new-session-drafts.d.ts +12 -0
- package/dist/organization-membership-lifecycle.d.ts +8 -1
- package/dist/organization-model-providers.d.ts +101 -0
- package/dist/pack-components.d.ts +1 -0
- package/dist/pr-review.d.ts +9 -0
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -8
- package/dist/schema.d.ts +7708 -5050
- package/dist/schema.js +25 -1
- package/dist/session-background-commands.d.ts +18 -6
- package/dist/session-background-commands.js +15 -9
- package/dist/session-control.d.ts +37 -2
- package/dist/session-goal-pacing.d.ts +3 -6
- package/dist/session-queue-commands.d.ts +17 -0
- package/dist/session-tenancy.js +3 -3
- package/dist/tool-gateway-approvals.d.ts +31 -0
- package/dist/video-generation.js +3 -3
- package/dist/workspace-artifacts.d.ts +17 -3
- package/dist/xai-subscription.d.ts +8 -0
- package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
- package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
- package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
- package/drizzle/0390_organization_model_provider_connections.sql +197 -0
- package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
- package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
- package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
- package/drizzle/0394_session_selected_skill_activation.sql +20 -0
- package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
- package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
- package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
- package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
- package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
- package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
- package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
- package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
- package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
- package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
- package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
- package/drizzle/0406_workspace_html_site_sources.sql +71 -0
- package/package.json +6 -6
- package/src/automations.ts +9 -1
- package/src/capability-integrations.ts +8 -0
- package/src/codemode-operations.ts +31 -21
- package/src/codex-token-resolver.ts +16 -6
- package/src/connection-token-resolver.ts +52 -3
- package/src/generated-images.ts +70 -6
- package/src/index.ts +3894 -994
- package/src/insights-model-bundle.ts +54 -5
- package/src/insights.ts +53 -7
- package/src/managed-user-setup.ts +14 -3
- package/src/mcp-oauth.ts +488 -0
- package/src/migrate.ts +9 -1
- package/src/model-catalog.ts +682 -0
- package/src/model-context-snapshots.ts +151 -0
- package/src/new-session-drafts.ts +66 -21
- package/src/organization-membership-lifecycle.ts +47 -5
- package/src/organization-model-providers.ts +614 -0
- package/src/pack-components.ts +15 -2
- package/src/pr-review.ts +19 -0
- package/src/provision-roles.ts +4 -0
- package/src/runtime-posture.ts +64 -0
- package/src/schema.ts +666 -61
- package/src/session-background-commands.ts +180 -78
- package/src/session-control.ts +181 -12
- package/src/session-goal-pacing.ts +4 -8
- package/src/session-queue-commands.ts +157 -82
- package/src/tool-gateway-approvals.ts +168 -0
- package/src/workspace-artifacts.ts +633 -178
- package/src/xai-subscription.ts +66 -14
- package/dist/chunk-4NAR7FBA.js.map +0 -1
- package/dist/chunk-6DPJA36C.js.map +0 -1
- package/dist/chunk-O56DM5GZ.js.map +0 -1
- package/dist/chunk-S3TDDCS2.js.map +0 -1
- /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
- /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
- /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
- /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
- /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
- /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
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
:
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
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
|
+
}
|