@opengeni/db 3.7.4 → 3.8.2-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 (77) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/{chunk-O56DM5GZ.js → chunk-53A2PEHT.js} +328 -31
  4. package/dist/chunk-53A2PEHT.js.map +1 -0
  5. package/dist/{chunk-SZAWDXQC.js → chunk-5S4BQAHX.js} +3 -3
  6. package/dist/{chunk-S3TDDCS2.js → chunk-CITYUGMW.js} +3 -3
  7. package/dist/{chunk-4NAR7FBA.js → chunk-F5YOZEYQ.js} +49 -1
  8. package/dist/chunk-F5YOZEYQ.js.map +1 -0
  9. package/dist/{chunk-F6PBS6WE.js → chunk-HZV7O6JR.js} +2 -2
  10. package/dist/{chunk-5NNCGFQY.js → chunk-RLQVPOVR.js} +2 -2
  11. package/dist/{chunk-DZCYW2WT.js → chunk-UKOGHUGC.js} +2 -2
  12. package/dist/{chunk-US4IS6IV.js → chunk-X6TO5HII.js} +2 -2
  13. package/dist/{chunk-U4WPPEBH.js → chunk-Y53WEFYB.js} +2 -2
  14. package/dist/connection-token-resolver.d.ts +4 -0
  15. package/dist/editable-artifact-durable-export.js +2 -2
  16. package/dist/editable-artifacts.js +3 -3
  17. package/dist/index.d.ts +164 -14
  18. package/dist/index.js +7327 -5442
  19. package/dist/index.js.map +1 -1
  20. package/dist/insights.d.ts +9 -0
  21. package/dist/managed-auth-session-sets.js +3 -3
  22. package/dist/model-catalog.d.ts +153 -0
  23. package/dist/new-session-drafts.d.ts +12 -0
  24. package/dist/organization-membership-lifecycle.d.ts +8 -1
  25. package/dist/organization-model-providers.d.ts +101 -0
  26. package/dist/pack-components.d.ts +1 -0
  27. package/dist/pr-review.d.ts +9 -0
  28. package/dist/provision-roles.js +1 -1
  29. package/dist/runtime-posture.d.ts +7 -7
  30. package/dist/schema.d.ts +989 -2
  31. package/dist/schema.js +11 -1
  32. package/dist/session-background-commands.js +3 -3
  33. package/dist/session-control.d.ts +35 -0
  34. package/dist/session-queue-commands.d.ts +17 -0
  35. package/dist/session-tenancy.js +3 -3
  36. package/dist/video-generation.js +3 -3
  37. package/dist/xai-subscription.d.ts +8 -0
  38. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  39. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  40. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  41. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  42. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  43. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  44. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  45. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  46. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  47. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  48. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  49. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  50. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  51. package/package.json +6 -6
  52. package/src/automations.ts +9 -1
  53. package/src/connection-token-resolver.ts +12 -0
  54. package/src/index.ts +1551 -272
  55. package/src/insights-model-bundle.ts +54 -5
  56. package/src/insights.ts +53 -7
  57. package/src/model-catalog.ts +682 -0
  58. package/src/new-session-drafts.ts +66 -21
  59. package/src/organization-membership-lifecycle.ts +47 -5
  60. package/src/organization-model-providers.ts +614 -0
  61. package/src/pack-components.ts +15 -2
  62. package/src/pr-review.ts +19 -0
  63. package/src/provision-roles.ts +2 -0
  64. package/src/runtime-posture.ts +48 -0
  65. package/src/schema.ts +348 -32
  66. package/src/session-control.ts +170 -0
  67. package/src/session-queue-commands.ts +157 -82
  68. package/src/xai-subscription.ts +66 -14
  69. package/dist/chunk-4NAR7FBA.js.map +0 -1
  70. package/dist/chunk-O56DM5GZ.js.map +0 -1
  71. /package/dist/{chunk-SZAWDXQC.js.map → chunk-5S4BQAHX.js.map} +0 -0
  72. /package/dist/{chunk-S3TDDCS2.js.map → chunk-CITYUGMW.js.map} +0 -0
  73. /package/dist/{chunk-F6PBS6WE.js.map → chunk-HZV7O6JR.js.map} +0 -0
  74. /package/dist/{chunk-5NNCGFQY.js.map → chunk-RLQVPOVR.js.map} +0 -0
  75. /package/dist/{chunk-DZCYW2WT.js.map → chunk-UKOGHUGC.js.map} +0 -0
  76. /package/dist/{chunk-US4IS6IV.js.map → chunk-X6TO5HII.js.map} +0 -0
  77. /package/dist/{chunk-U4WPPEBH.js.map → chunk-Y53WEFYB.js.map} +0 -0
@@ -0,0 +1,614 @@
1
+ import { createHash } from "node:crypto";
2
+ import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
3
+ import { and, count, eq, exists, isNull, sql } from "drizzle-orm";
4
+
5
+ import { type Database, setSubjectRlsContext, withRlsContext } from "./database";
6
+ import { decryptEnvironmentValue } from "./environment-crypto";
7
+ import * as schema from "./schema";
8
+
9
+ export type OrganizationModelProviderKind = "vercel_gateway" | "openrouter";
10
+ export type OrganizationModelProviderConnection = {
11
+ providerKind: OrganizationModelProviderKind;
12
+ status: "active" | "revoked";
13
+ version: number;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ };
17
+ export type OrganizationModelProviderCustomModel = {
18
+ id: string;
19
+ accountId: string;
20
+ providerKind: OrganizationModelProviderKind;
21
+ upstreamModelId: string;
22
+ label: string | null;
23
+ version: number;
24
+ retiredAt: Date | null;
25
+ createdAt: Date;
26
+ updatedAt: Date;
27
+ };
28
+
29
+ export class OrganizationModelProviderConflictError extends Error {}
30
+ export class OrganizationModelProviderLimitError extends Error {}
31
+
32
+ function digest(value: unknown): string {
33
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
34
+ }
35
+
36
+ export function organizationModelProviderCredentialDigest(credential: string): string {
37
+ return createHash("sha256").update(credential).digest("hex");
38
+ }
39
+
40
+ async function withOrganizationProviderAdministrator<T>(
41
+ db: Database,
42
+ input: { organizationId: string; actorSubjectId: string },
43
+ use: (scopedDb: Database) => Promise<T>,
44
+ ): Promise<T> {
45
+ return await withRlsContext(
46
+ db,
47
+ { accountId: input.organizationId, workspaceId: null },
48
+ async (scopedDb) => {
49
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
50
+ await scopedDb.execute(sql`
51
+ select get_organization_administration_overview(
52
+ ${input.organizationId}::uuid, ${input.actorSubjectId}
53
+ )
54
+ `);
55
+ return await use(scopedDb);
56
+ },
57
+ );
58
+ }
59
+
60
+ function mapConnection(
61
+ row: typeof schema.organizationModelProviderConnections.$inferSelect,
62
+ ): OrganizationModelProviderConnection {
63
+ return {
64
+ providerKind: row.providerKind,
65
+ status: row.status,
66
+ version: row.version,
67
+ createdAt: row.createdAt,
68
+ updatedAt: row.updatedAt,
69
+ };
70
+ }
71
+
72
+ function mapConnectionOperation(
73
+ row: typeof schema.organizationModelProviderConnectionOperations.$inferSelect,
74
+ ): OrganizationModelProviderConnection {
75
+ return {
76
+ providerKind: row.providerKind,
77
+ status: row.resultStatus,
78
+ version: row.resultVersion,
79
+ createdAt: row.resultCreatedAt,
80
+ updatedAt: row.resultUpdatedAt,
81
+ };
82
+ }
83
+
84
+ async function recordConnectionOperation(
85
+ db: Database,
86
+ input: {
87
+ accountId: string;
88
+ operationId: string;
89
+ requestHash: string;
90
+ connection: OrganizationModelProviderConnection;
91
+ },
92
+ ): Promise<void> {
93
+ await db.insert(schema.organizationModelProviderConnectionOperations).values({
94
+ accountId: input.accountId,
95
+ providerKind: input.connection.providerKind,
96
+ operationId: input.operationId,
97
+ requestHash: input.requestHash,
98
+ resultStatus: input.connection.status,
99
+ resultVersion: input.connection.version,
100
+ resultCreatedAt: input.connection.createdAt,
101
+ resultUpdatedAt: input.connection.updatedAt,
102
+ });
103
+ }
104
+
105
+ function mapModel(
106
+ row: typeof schema.organizationModelProviderCustomModels.$inferSelect,
107
+ ): OrganizationModelProviderCustomModel {
108
+ return {
109
+ id: row.id,
110
+ accountId: row.accountId,
111
+ providerKind: row.providerKind,
112
+ upstreamModelId: row.upstreamModelId,
113
+ label: row.label,
114
+ version: row.version,
115
+ retiredAt: row.retiredAt,
116
+ createdAt: row.createdAt,
117
+ updatedAt: row.updatedAt,
118
+ };
119
+ }
120
+
121
+ export async function getOrganizationModelProviderConnection(
122
+ db: Database,
123
+ input: {
124
+ organizationId: string;
125
+ actorSubjectId: string;
126
+ providerKind: OrganizationModelProviderKind;
127
+ },
128
+ ): Promise<OrganizationModelProviderConnection | null> {
129
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
130
+ const [row] = await scopedDb
131
+ .select()
132
+ .from(schema.organizationModelProviderConnections)
133
+ .where(
134
+ and(
135
+ eq(schema.organizationModelProviderConnections.accountId, input.organizationId),
136
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
137
+ ),
138
+ )
139
+ .limit(1);
140
+ return row ? mapConnection(row) : null;
141
+ });
142
+ }
143
+
144
+ export async function upsertOrganizationModelProviderConnection(
145
+ db: Database,
146
+ input: {
147
+ organizationId: string;
148
+ actorSubjectId: string;
149
+ providerKind: OrganizationModelProviderKind;
150
+ credentialEncrypted: string;
151
+ credentialDigest: string;
152
+ operationId: string;
153
+ expectedVersion?: number;
154
+ },
155
+ ): Promise<OrganizationModelProviderConnection> {
156
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
157
+ await scopedDb.execute(
158
+ sql`select pg_advisory_xact_lock(hashtextextended(${`organization-model-provider:${input.organizationId}:${input.providerKind}`}, 0))`,
159
+ );
160
+ const requestHash = digest({
161
+ operation: "upsert",
162
+ expectedVersion: input.expectedVersion,
163
+ credentialDigest: input.credentialDigest,
164
+ });
165
+ const [replay] = await scopedDb
166
+ .select()
167
+ .from(schema.organizationModelProviderConnectionOperations)
168
+ .where(
169
+ and(
170
+ eq(schema.organizationModelProviderConnectionOperations.accountId, input.organizationId),
171
+ eq(schema.organizationModelProviderConnectionOperations.providerKind, input.providerKind),
172
+ eq(schema.organizationModelProviderConnectionOperations.operationId, input.operationId),
173
+ ),
174
+ )
175
+ .limit(1);
176
+ if (replay) {
177
+ if (replay.requestHash !== requestHash) throw new OrganizationModelProviderConflictError();
178
+ return mapConnectionOperation(replay);
179
+ }
180
+ const [existing] = await scopedDb
181
+ .select()
182
+ .from(schema.organizationModelProviderConnections)
183
+ .where(
184
+ and(
185
+ eq(schema.organizationModelProviderConnections.accountId, input.organizationId),
186
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
187
+ ),
188
+ )
189
+ .for("update")
190
+ .limit(1);
191
+ const expected = input.expectedVersion ?? 0;
192
+ if ((existing?.version ?? 0) !== expected) throw new OrganizationModelProviderConflictError();
193
+ const [row] = existing
194
+ ? await scopedDb
195
+ .update(schema.organizationModelProviderConnections)
196
+ .set({
197
+ status: "active",
198
+ credentialEncrypted: input.credentialEncrypted,
199
+ version: existing.version + 1,
200
+ operationId: input.operationId,
201
+ requestHash,
202
+ updatedBySubjectId: input.actorSubjectId,
203
+ updatedAt: new Date(),
204
+ })
205
+ .where(eq(schema.organizationModelProviderConnections.id, existing.id))
206
+ .returning()
207
+ : await scopedDb
208
+ .insert(schema.organizationModelProviderConnections)
209
+ .values({
210
+ accountId: input.organizationId,
211
+ providerKind: input.providerKind,
212
+ credentialEncrypted: input.credentialEncrypted,
213
+ operationId: input.operationId,
214
+ requestHash,
215
+ updatedBySubjectId: input.actorSubjectId,
216
+ })
217
+ .returning();
218
+ if (!row) throw new Error("organization provider connection write returned no row");
219
+ const connection = mapConnection(row);
220
+ await recordConnectionOperation(scopedDb, {
221
+ accountId: input.organizationId,
222
+ operationId: input.operationId,
223
+ requestHash,
224
+ connection,
225
+ });
226
+ return connection;
227
+ });
228
+ }
229
+
230
+ export async function revokeOrganizationModelProviderConnection(
231
+ db: Database,
232
+ input: {
233
+ organizationId: string;
234
+ actorSubjectId: string;
235
+ providerKind: OrganizationModelProviderKind;
236
+ operationId: string;
237
+ expectedVersion: number;
238
+ },
239
+ ): Promise<OrganizationModelProviderConnection> {
240
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
241
+ await scopedDb.execute(
242
+ sql`select pg_advisory_xact_lock(hashtextextended(${`organization-model-provider:${input.organizationId}:${input.providerKind}`}, 0))`,
243
+ );
244
+ await scopedDb.execute(
245
+ sql`select pg_advisory_xact_lock(hashtextextended(${`organization-model-provider-models:${input.organizationId}:${input.providerKind}`}, 0))`,
246
+ );
247
+ const requestHash = digest({ operation: "revoke", expectedVersion: input.expectedVersion });
248
+ const [replay] = await scopedDb
249
+ .select()
250
+ .from(schema.organizationModelProviderConnectionOperations)
251
+ .where(
252
+ and(
253
+ eq(schema.organizationModelProviderConnectionOperations.accountId, input.organizationId),
254
+ eq(schema.organizationModelProviderConnectionOperations.providerKind, input.providerKind),
255
+ eq(schema.organizationModelProviderConnectionOperations.operationId, input.operationId),
256
+ ),
257
+ )
258
+ .limit(1);
259
+ if (replay) {
260
+ if (replay.requestHash !== requestHash) throw new OrganizationModelProviderConflictError();
261
+ return mapConnectionOperation(replay);
262
+ }
263
+ const [existing] = await scopedDb
264
+ .select()
265
+ .from(schema.organizationModelProviderConnections)
266
+ .where(
267
+ and(
268
+ eq(schema.organizationModelProviderConnections.accountId, input.organizationId),
269
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
270
+ ),
271
+ )
272
+ .for("update")
273
+ .limit(1);
274
+ if (!existing) throw new OrganizationModelProviderConflictError();
275
+ if (existing.version !== input.expectedVersion)
276
+ throw new OrganizationModelProviderConflictError();
277
+ const [row] = await scopedDb
278
+ .update(schema.organizationModelProviderConnections)
279
+ .set({
280
+ status: "revoked",
281
+ credentialEncrypted: "",
282
+ version: existing.version + 1,
283
+ operationId: input.operationId,
284
+ requestHash,
285
+ updatedBySubjectId: input.actorSubjectId,
286
+ updatedAt: new Date(),
287
+ })
288
+ .where(eq(schema.organizationModelProviderConnections.id, existing.id))
289
+ .returning();
290
+ if (!row) throw new Error("organization provider revoke returned no row");
291
+ const connection = mapConnection(row);
292
+ await recordConnectionOperation(scopedDb, {
293
+ accountId: input.organizationId,
294
+ operationId: input.operationId,
295
+ requestHash,
296
+ connection,
297
+ });
298
+ return connection;
299
+ });
300
+ }
301
+
302
+ export async function organizationModelProviderConnectionActiveForWorkspace(
303
+ db: Database,
304
+ input: { accountId: string; workspaceId: string; providerKind: OrganizationModelProviderKind },
305
+ ): Promise<boolean> {
306
+ return await withRlsContext(db, input, async (scopedDb) => {
307
+ const [row] = await scopedDb
308
+ .select({ id: schema.organizationModelProviderConnections.id })
309
+ .from(schema.organizationModelProviderConnections)
310
+ .where(
311
+ and(
312
+ eq(schema.organizationModelProviderConnections.accountId, input.accountId),
313
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
314
+ eq(schema.organizationModelProviderConnections.status, "active"),
315
+ ),
316
+ )
317
+ .limit(1);
318
+ return Boolean(row);
319
+ });
320
+ }
321
+
322
+ export async function loadOrganizationModelProviderApiKey(
323
+ db: Database,
324
+ settings: Settings,
325
+ input: { accountId: string; workspaceId: string; providerKind: OrganizationModelProviderKind },
326
+ ): Promise<string | null> {
327
+ const key = environmentsEncryptionKeyBytes(settings);
328
+ if (!key) return null;
329
+ return await withRlsContext(db, input, async (scopedDb) => {
330
+ const [row] = await scopedDb
331
+ .select({
332
+ credentialEncrypted: schema.organizationModelProviderConnections.credentialEncrypted,
333
+ })
334
+ .from(schema.organizationModelProviderConnections)
335
+ .where(
336
+ and(
337
+ eq(schema.organizationModelProviderConnections.accountId, input.accountId),
338
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
339
+ eq(schema.organizationModelProviderConnections.status, "active"),
340
+ ),
341
+ )
342
+ .limit(1);
343
+ return row ? decryptEnvironmentValue(key, row.credentialEncrypted) : null;
344
+ });
345
+ }
346
+
347
+ export async function listOrganizationModelProviderCustomModelsForWorkspace(
348
+ db: Database,
349
+ input: {
350
+ accountId: string;
351
+ workspaceId: string;
352
+ providerKind: OrganizationModelProviderKind;
353
+ includeRetired?: boolean;
354
+ },
355
+ ): Promise<OrganizationModelProviderCustomModel[]> {
356
+ return await withRlsContext(db, input, async (scopedDb) => {
357
+ const rows = await scopedDb
358
+ .select()
359
+ .from(schema.organizationModelProviderCustomModels)
360
+ .where(
361
+ and(
362
+ eq(schema.organizationModelProviderCustomModels.accountId, input.accountId),
363
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
364
+ ...(input.includeRetired
365
+ ? []
366
+ : [isNull(schema.organizationModelProviderCustomModels.retiredAt)]),
367
+ ),
368
+ );
369
+ return rows.map(mapModel);
370
+ });
371
+ }
372
+
373
+ export async function getOrganizationModelProviderCustomModelForExecution(
374
+ db: Database,
375
+ input: {
376
+ accountId: string;
377
+ workspaceId: string;
378
+ providerKind: OrganizationModelProviderKind;
379
+ upstreamModelId: string;
380
+ },
381
+ ): Promise<OrganizationModelProviderCustomModel | null> {
382
+ return await withRlsContext(db, input, async (scopedDb) => {
383
+ const [row] = await scopedDb
384
+ .select()
385
+ .from(schema.organizationModelProviderCustomModels)
386
+ .where(
387
+ and(
388
+ eq(schema.organizationModelProviderCustomModels.accountId, input.accountId),
389
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
390
+ eq(schema.organizationModelProviderCustomModels.upstreamModelId, input.upstreamModelId),
391
+ ),
392
+ )
393
+ .orderBy(
394
+ sql`(${schema.organizationModelProviderCustomModels.retiredAt} is null) desc`,
395
+ sql`${schema.organizationModelProviderCustomModels.updatedAt} desc`,
396
+ )
397
+ .limit(1);
398
+ return row ? mapModel(row) : null;
399
+ });
400
+ }
401
+
402
+ export async function withOrganizationModelProviderCustomModelReadLock<T>(
403
+ db: Database,
404
+ input: { accountId: string; workspaceId: string; providerKind: OrganizationModelProviderKind },
405
+ use: (scopedDb: Database) => Promise<T>,
406
+ ): Promise<T> {
407
+ return await withRlsContext(db, input, async (scopedDb) => {
408
+ await scopedDb.execute(
409
+ sql`select pg_advisory_xact_lock_shared(hashtextextended(${`organization-model-provider-models:${input.accountId}:${input.providerKind}`}, 0))`,
410
+ );
411
+ return await use(scopedDb);
412
+ });
413
+ }
414
+
415
+ export async function lockActiveOrganizationModelProviderCustomModelForAdmission(
416
+ db: Database,
417
+ input: {
418
+ accountId: string;
419
+ workspaceId: string;
420
+ providerKind: OrganizationModelProviderKind;
421
+ upstreamModelId: string;
422
+ },
423
+ ): Promise<OrganizationModelProviderCustomModel | null> {
424
+ return await withOrganizationModelProviderCustomModelReadLock(db, input, async (scopedDb) => {
425
+ const [row] = await scopedDb
426
+ .select()
427
+ .from(schema.organizationModelProviderCustomModels)
428
+ .where(
429
+ and(
430
+ eq(schema.organizationModelProviderCustomModels.accountId, input.accountId),
431
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
432
+ eq(schema.organizationModelProviderCustomModels.upstreamModelId, input.upstreamModelId),
433
+ isNull(schema.organizationModelProviderCustomModels.retiredAt),
434
+ exists(
435
+ scopedDb
436
+ .select({ id: schema.organizationModelProviderConnections.id })
437
+ .from(schema.organizationModelProviderConnections)
438
+ .where(
439
+ and(
440
+ eq(schema.organizationModelProviderConnections.accountId, input.accountId),
441
+ eq(schema.organizationModelProviderConnections.providerKind, input.providerKind),
442
+ eq(schema.organizationModelProviderConnections.status, "active"),
443
+ ),
444
+ ),
445
+ ),
446
+ ),
447
+ )
448
+ .limit(1);
449
+ return row ? mapModel(row) : null;
450
+ });
451
+ }
452
+
453
+ export async function listOrganizationModelProviderCustomModels(
454
+ db: Database,
455
+ input: {
456
+ organizationId: string;
457
+ actorSubjectId: string;
458
+ providerKind: OrganizationModelProviderKind;
459
+ },
460
+ ): Promise<OrganizationModelProviderCustomModel[]> {
461
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
462
+ const rows = await scopedDb
463
+ .select()
464
+ .from(schema.organizationModelProviderCustomModels)
465
+ .where(
466
+ and(
467
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
468
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
469
+ isNull(schema.organizationModelProviderCustomModels.retiredAt),
470
+ ),
471
+ );
472
+ return rows.map(mapModel);
473
+ });
474
+ }
475
+
476
+ export async function createOrganizationModelProviderCustomModel(
477
+ db: Database,
478
+ input: {
479
+ organizationId: string;
480
+ actorSubjectId: string;
481
+ providerKind: OrganizationModelProviderKind;
482
+ upstreamModelId: string;
483
+ label?: string;
484
+ operationId: string;
485
+ },
486
+ ): Promise<OrganizationModelProviderCustomModel> {
487
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
488
+ await scopedDb.execute(
489
+ sql`select pg_advisory_xact_lock(hashtextextended(${`organization-model-provider-models:${input.organizationId}:${input.providerKind}`}, 0))`,
490
+ );
491
+ const requestHash = digest({
492
+ upstreamModelId: input.upstreamModelId,
493
+ label: input.label ?? null,
494
+ });
495
+ const [replay] = await scopedDb
496
+ .select()
497
+ .from(schema.organizationModelProviderCustomModels)
498
+ .where(
499
+ and(
500
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
501
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
502
+ eq(schema.organizationModelProviderCustomModels.createOperationId, input.operationId),
503
+ ),
504
+ )
505
+ .limit(1);
506
+ if (replay) {
507
+ if (replay.createRequestHash !== requestHash)
508
+ throw new OrganizationModelProviderConflictError();
509
+ return mapModel(replay);
510
+ }
511
+ const [{ active = 0, total = 0 } = {}] = await scopedDb
512
+ .select({
513
+ active: count(
514
+ sql`case when ${schema.organizationModelProviderCustomModels.retiredAt} is null then 1 end`,
515
+ ),
516
+ total: count(),
517
+ })
518
+ .from(schema.organizationModelProviderCustomModels)
519
+ .where(
520
+ and(
521
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
522
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
523
+ ),
524
+ );
525
+ if (Number(active) >= 100 || Number(total) >= 1000)
526
+ throw new OrganizationModelProviderLimitError();
527
+ const [duplicate] = await scopedDb
528
+ .select({ id: schema.organizationModelProviderCustomModels.id })
529
+ .from(schema.organizationModelProviderCustomModels)
530
+ .where(
531
+ and(
532
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
533
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
534
+ eq(schema.organizationModelProviderCustomModels.upstreamModelId, input.upstreamModelId),
535
+ isNull(schema.organizationModelProviderCustomModels.retiredAt),
536
+ ),
537
+ )
538
+ .limit(1);
539
+ if (duplicate) throw new OrganizationModelProviderConflictError();
540
+ const [row] = await scopedDb
541
+ .insert(schema.organizationModelProviderCustomModels)
542
+ .values({
543
+ accountId: input.organizationId,
544
+ providerKind: input.providerKind,
545
+ upstreamModelId: input.upstreamModelId,
546
+ label: input.label ?? null,
547
+ createOperationId: input.operationId,
548
+ createRequestHash: requestHash,
549
+ createdBySubjectId: input.actorSubjectId,
550
+ })
551
+ .returning();
552
+ if (!row) throw new Error("organization custom model create returned no row");
553
+ return mapModel(row);
554
+ });
555
+ }
556
+
557
+ export async function retireOrganizationModelProviderCustomModel(
558
+ db: Database,
559
+ input: {
560
+ organizationId: string;
561
+ actorSubjectId: string;
562
+ providerKind: OrganizationModelProviderKind;
563
+ customModelId: string;
564
+ expectedVersion: number;
565
+ operationId: string;
566
+ },
567
+ ): Promise<OrganizationModelProviderCustomModel> {
568
+ return await withOrganizationProviderAdministrator(db, input, async (scopedDb) => {
569
+ await scopedDb.execute(
570
+ sql`select pg_advisory_xact_lock(hashtextextended(${`organization-model-provider-models:${input.organizationId}:${input.providerKind}`}, 0))`,
571
+ );
572
+ const requestHash = digest({
573
+ customModelId: input.customModelId,
574
+ expectedVersion: input.expectedVersion,
575
+ });
576
+ const [replay] = await scopedDb
577
+ .select()
578
+ .from(schema.organizationModelProviderCustomModels)
579
+ .where(
580
+ and(
581
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
582
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
583
+ eq(schema.organizationModelProviderCustomModels.deleteOperationId, input.operationId),
584
+ ),
585
+ )
586
+ .limit(1);
587
+ if (replay) {
588
+ if (replay.deleteRequestHash !== requestHash)
589
+ throw new OrganizationModelProviderConflictError();
590
+ return mapModel(replay);
591
+ }
592
+ const [row] = await scopedDb
593
+ .update(schema.organizationModelProviderCustomModels)
594
+ .set({
595
+ retiredAt: new Date(),
596
+ deleteOperationId: input.operationId,
597
+ deleteRequestHash: requestHash,
598
+ version: input.expectedVersion + 1,
599
+ updatedAt: new Date(),
600
+ })
601
+ .where(
602
+ and(
603
+ eq(schema.organizationModelProviderCustomModels.accountId, input.organizationId),
604
+ eq(schema.organizationModelProviderCustomModels.providerKind, input.providerKind),
605
+ eq(schema.organizationModelProviderCustomModels.id, input.customModelId),
606
+ eq(schema.organizationModelProviderCustomModels.version, input.expectedVersion),
607
+ isNull(schema.organizationModelProviderCustomModels.retiredAt),
608
+ ),
609
+ )
610
+ .returning();
611
+ if (!row) throw new OrganizationModelProviderConflictError();
612
+ return mapModel(row);
613
+ });
614
+ }
@@ -29,6 +29,7 @@ export type PackInlineSkillRequirement = {
29
29
  key: string;
30
30
  capabilityId: string;
31
31
  name: string;
32
+ activationMode: "workspace_managed" | "session_selected";
32
33
  contentSha256: string;
33
34
  };
34
35
 
@@ -97,9 +98,14 @@ export async function resolvePackInlineSkillReferences(
97
98
  .select({
98
99
  facetInstallationId: schema.capabilityFacetInstallations.id,
99
100
  name: schema.capabilitySkillFacets.name,
101
+ activationMode: schema.capabilityFacets.activationMode,
100
102
  contentSha256: schema.capabilitySkillFacets.contentSha256,
101
103
  })
102
104
  .from(schema.capabilitySkillFacets)
105
+ .innerJoin(
106
+ schema.capabilityFacets,
107
+ eq(schema.capabilityFacets.id, schema.capabilitySkillFacets.facetId),
108
+ )
103
109
  .innerJoin(
104
110
  schema.capabilityFacetInstallations,
105
111
  eq(schema.capabilityFacetInstallations.facetId, schema.capabilitySkillFacets.facetId),
@@ -141,9 +147,16 @@ export async function resolvePackInlineSkillReferences(
141
147
  (row) => row.name.toLowerCase() === requirement.name.toLowerCase(),
142
148
  );
143
149
  const exact = candidates.find(
144
- (candidate) => candidate.contentSha256 === requirement.contentSha256,
150
+ (candidate) =>
151
+ candidate.contentSha256 === requirement.contentSha256 &&
152
+ candidate.activationMode === requirement.activationMode,
145
153
  );
146
- const mismatch = candidates[0] ?? null;
154
+ const mismatch =
155
+ candidates.find(
156
+ (candidate) =>
157
+ candidate.contentSha256 !== requirement.contentSha256 ||
158
+ candidate.activationMode !== requirement.activationMode,
159
+ ) ?? null;
147
160
  return {
148
161
  key: requirement.key,
149
162
  kind: "inline_skill" as const,
package/src/pr-review.ts CHANGED
@@ -318,6 +318,8 @@ export async function createPrReviewRepositoryBinding(
318
318
  eventTypes: string[];
319
319
  configuration: Record<string, unknown>;
320
320
  sessionTemplate: AutomationSessionTemplate;
321
+ /** Trusted database-only admission seam. Throwing rolls the binding and trigger back. */
322
+ beforeCreateCommit?: (tx: Database) => Promise<void>;
321
323
  },
322
324
  ): Promise<PrReviewRepositoryBinding> {
323
325
  return await withRlsContext(db, input, async (scopedDb) => {
@@ -345,6 +347,7 @@ export async function createPrReviewRepositoryBinding(
345
347
  "PR Review repository provider does not match its app",
346
348
  );
347
349
  }
350
+ await input.beforeCreateCommit?.(tx);
348
351
  const bindingId = randomUUID();
349
352
  const [trigger] = await tx
350
353
  .insert(schema.automationTriggers)
@@ -842,6 +845,16 @@ export async function updatePrReviewRepositoryBinding(
842
845
  model?: string | null;
843
846
  additionalInstructions?: string | null;
844
847
  status?: "active" | "disabled";
848
+ /** Trusted database-only admission seam. Throwing rolls the binding revision back. */
849
+ beforeUpdateCommit?: (
850
+ tx: Database,
851
+ context: {
852
+ currentModel: string | null;
853
+ currentStatus: "active" | "disabled";
854
+ nextModel: string | null;
855
+ nextStatus: "active" | "disabled";
856
+ },
857
+ ) => Promise<void>;
845
858
  },
846
859
  ): Promise<PrReviewRepositoryBinding | null> {
847
860
  return await withRlsContext(db, input, async (scopedDb) => {
@@ -891,6 +904,12 @@ export async function updatePrReviewRepositoryBinding(
891
904
  : input.additionalInstructions,
892
905
  status: input.status ?? (current.binding.status as "active" | "disabled"),
893
906
  };
907
+ await input.beforeUpdateCommit?.(tx, {
908
+ currentModel: current.binding.model,
909
+ currentStatus: current.binding.status as "active" | "disabled",
910
+ nextModel: next.model,
911
+ nextStatus: next.status,
912
+ });
894
913
  const nextRevision = current.trigger.currentRevision + 1;
895
914
  await tx.insert(schema.automationTriggerRevisions).values({
896
915
  triggerId: current.trigger.id,