@opengeni/core 1.3.0 → 1.4.2

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 (36) hide show
  1. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  2. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  3. package/dist/domain/capabilities.d.ts +1 -0
  4. package/dist/domain/connector-tool-permissions.d.ts +1 -1
  5. package/dist/domain/realtime-voice-billing.d.ts +95 -0
  6. package/dist/domain/scheduled-task-access.d.ts +2 -2
  7. package/dist/domain/scheduled-tasks.d.ts +2 -2
  8. package/dist/domain/session-connection-accounts.d.ts +1 -1
  9. package/dist/domain/session-start-timing.d.ts +1 -1
  10. package/dist/domain/session-tool-policy.d.ts +11 -7
  11. package/dist/domain/voice-input-billing.d.ts +77 -0
  12. package/dist/domain/web-search-billing.d.ts +45 -0
  13. package/dist/goal-admission.d.ts +24 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +1065 -365
  16. package/dist/index.js.map +1 -1
  17. package/dist/transcription.d.ts +119 -2
  18. package/package.json +12 -12
  19. package/src/application/modal-native-live-declaration-join.ts +196 -0
  20. package/src/application/modal-native-original-configuration.ts +261 -0
  21. package/src/default-session-model.ts +58 -88
  22. package/src/domain/capabilities.ts +44 -2
  23. package/src/domain/connector-tool-permissions.ts +24 -8
  24. package/src/domain/realtime-voice-billing.ts +400 -0
  25. package/src/domain/scheduled-task-access.ts +2 -1
  26. package/src/domain/scheduled-tasks.ts +3 -2
  27. package/src/domain/session-connection-accounts.ts +7 -3
  28. package/src/domain/session-start-timing.ts +5 -0
  29. package/src/domain/session-tool-policy.ts +20 -4
  30. package/src/domain/sessions.ts +145 -98
  31. package/src/domain/voice-input-billing.ts +315 -0
  32. package/src/domain/web-search-billing.ts +154 -0
  33. package/src/index.ts +3 -0
  34. package/src/model-catalog.ts +32 -38
  35. package/src/sandbox/routing.ts +59 -25
  36. package/src/transcription.ts +122 -3
@@ -22,15 +22,11 @@ import {
22
22
  getWorkspace,
23
23
  getWorkspaceConnectionModelRestrictions,
24
24
  getWorkspaceModelPolicy,
25
- getWorkspaceProviderApiKeyConnectionMetadata,
26
- listWorkspaceProviderCustomModels,
27
- listOrganizationModelProviderCustomModelsForWorkspace,
28
- listWorkspaceGatewayCustomModels,
29
- listWorkspaceOpenRouterCustomModels,
30
- organizationModelProviderConnectionActiveForWorkspace,
25
+ getOrganizationModelProviderCatalogForWorkspace,
26
+ listConnectionsMetadata,
27
+ listWorkspaceProviderCustomModelsByKind,
31
28
  workspaceCodexSubscriptionActive,
32
- workspaceOpenRouterConnectionActive,
33
- workspaceVercelAiGatewayConnectionActive,
29
+ workspaceProviderApiKeyConnectionMetadataFromConnections,
34
30
  getBillingBalance,
35
31
  spendableCreditMicros,
36
32
  workspaceXaiSubscriptionActive,
@@ -41,6 +37,7 @@ import {
41
37
  workspaceClaudeSubscriptionActiveForAuthority,
42
38
  type ConnectionModelRestrictions,
43
39
  type Database,
40
+ type WorkspaceCustomModelProviderKind,
44
41
  } from "@opengeni/db";
45
42
  import {
46
43
  isWorkspaceModelAdmissible,
@@ -422,82 +419,43 @@ export async function loadWorkspaceModelSelectionInput(
422
419
  options: { observeAvailability?: boolean } = {},
423
420
  ): Promise<WorkspaceModelSelectionInput> {
424
421
  const { accountId, workspaceId } = context;
425
- const restrictionsRead = (async () =>
426
- connectionRestrictionsAndXaiReadiness(db, settings, context))();
427
- const claudePoolRead = (async () =>
428
- loadWorkspaceClaudeSubscriptionReadiness(db, settings, context))();
422
+ // The exact provider set the catalog route reads. A disabled Claude
423
+ // subscription contributes no catalog entry, so its rows are not read.
424
+ const providerKinds: WorkspaceCustomModelProviderKind[] = [
425
+ "vercel_gateway",
426
+ "openrouter",
427
+ ...CLAUDE_CONNECTION_KINDS.filter(
428
+ (kind) => kind !== "claude_subscription" || settings.claudeSubscriptionEnabled,
429
+ ),
430
+ ];
429
431
  const inputRead = (async () =>
430
432
  Promise.all([
431
- restrictionsRead,
432
- claudePoolRead,
433
+ connectionRestrictionsAndXaiReadiness(db, settings, context),
434
+ loadWorkspaceClaudeSubscriptionReadiness(db, settings, context),
433
435
  getWorkspaceModelPolicy(db, workspaceId),
434
436
  workspaceCodexSubscriptionActive(db, settings, workspaceId),
435
437
  options.observeAvailability === false
436
438
  ? Promise.resolve({})
437
439
  : loadWorkspaceCodexModelAvailability(db, settings, workspaceId),
438
- workspaceVercelAiGatewayConnectionActive(db, workspaceId),
439
- listWorkspaceGatewayCustomModels(db, { accountId, workspaceId }),
440
- workspaceOpenRouterConnectionActive(db, workspaceId),
441
- listWorkspaceOpenRouterCustomModels(db, { accountId, workspaceId }),
442
- organizationModelProviderConnectionActiveForWorkspace(db, {
443
- accountId,
444
- workspaceId,
445
- providerKind: "vercel_gateway",
446
- }),
447
- organizationModelProviderConnectionActiveForWorkspace(db, {
448
- accountId,
449
- workspaceId,
450
- providerKind: "openrouter",
451
- }),
452
- listOrganizationModelProviderCustomModelsForWorkspace(db, {
453
- accountId,
454
- workspaceId,
455
- providerKind: "vercel_gateway",
456
- }),
457
- listOrganizationModelProviderCustomModelsForWorkspace(db, {
458
- accountId,
459
- workspaceId,
460
- providerKind: "openrouter",
461
- }),
462
440
  ]))();
463
- const claudeConnections: ClaudeConnectionCatalog = {};
464
- const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
465
441
  // Transaction handles share one backend and LOCAL scope. Preserve the old
466
442
  // batch ordering there rather than introduce concurrent nested savepoints.
467
443
  if (typeof (db as Database & { rollback?: unknown }).rollback === "function") await inputRead;
468
- // Catalog metadata does not depend on the other providers' policy/readiness
469
- // batch. Only subscription activation joins its exact existing pool read.
470
- // These helpers keep their separate root-pool RLS scopes; no shared transaction.
444
+ // One scoped read per catalog family (workspace connection metadata,
445
+ // workspace custom models, organization provider readiness + models) instead
446
+ // of one scoped transaction per provider kind. The batched readers keep each
447
+ // provider's filters, bound and overflow error, exactly as the workspace
448
+ // model catalog route reads them.
471
449
  const catalogRead = (async () =>
472
- Promise.all(
473
- CLAUDE_CONNECTION_KINDS.map(async (kind) => {
474
- if (kind === "claude_subscription" && !settings.claudeSubscriptionEnabled) return;
475
- const [active, models, metadata, workspaceModels] = await Promise.all([
476
- kind === "claude_subscription"
477
- ? claudePoolRead.then((pool) => pool.organization)
478
- : organizationModelProviderConnectionActiveForWorkspace(db, {
479
- accountId,
480
- workspaceId,
481
- providerKind: kind,
482
- }),
483
- listOrganizationModelProviderCustomModelsForWorkspace(db, {
484
- accountId,
485
- workspaceId,
486
- providerKind: kind,
487
- }),
488
- kind === "claude_subscription"
489
- ? Promise.resolve(null)
490
- : getWorkspaceProviderApiKeyConnectionMetadata(db, workspaceId, kind),
491
- listWorkspaceProviderCustomModels(db, { accountId, workspaceId, providerKind: kind }),
492
- ]);
493
- claudeConnections[kind] = { active, models };
494
- workspaceClaudeConnections[kind] = {
495
- active:
496
- kind === "claude_subscription" ? (await claudePoolRead).workspace : metadata !== null,
497
- models: workspaceModels,
498
- };
450
+ Promise.all([
451
+ listConnectionsMetadata(db, workspaceId, null),
452
+ listWorkspaceProviderCustomModelsByKind(db, { accountId, workspaceId, providerKinds }),
453
+ getOrganizationModelProviderCatalogForWorkspace(db, {
454
+ accountId,
455
+ workspaceId,
456
+ providerKinds,
499
457
  }),
500
- ))();
458
+ ]))();
501
459
  // Observe both batches before admission resumes or fails. Retain the previous
502
460
  // input-batch error precedence even if the independent catalog fails first.
503
461
  const [inputResult, catalogResult] = await Promise.allSettled([inputRead, catalogRead]);
@@ -505,19 +463,31 @@ export async function loadWorkspaceModelSelectionInput(
505
463
  if (catalogResult.status === "rejected") throw catalogResult.reason;
506
464
  const [
507
465
  { restrictions: connectionModelRestrictions, xaiSubscriptionActive },
508
- _claudePool,
466
+ claudePool,
509
467
  policy,
510
468
  codexSubscriptionActive,
511
469
  observations,
512
- workspaceGatewayConnectionActive,
513
- workspaceGatewayCustomModels,
514
- openRouterConnectionActive,
515
- workspaceOpenRouterCustomModels,
516
- organizationGatewayConnectionActive,
517
- organizationOpenRouterConnectionActive,
518
- organizationGatewayCustomModels,
519
- organizationOpenRouterCustomModels,
520
470
  ] = inputResult.value;
471
+ const [workspaceConnections, workspaceCustomModels, organizationProviders] = catalogResult.value;
472
+ const workspaceConnectionActive = (kind: WorkspaceCustomModelProviderKind) =>
473
+ workspaceProviderApiKeyConnectionMetadataFromConnections(workspaceConnections, kind) !== null;
474
+ const claudeConnections: ClaudeConnectionCatalog = {};
475
+ const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
476
+ for (const kind of CLAUDE_CONNECTION_KINDS) {
477
+ if (kind === "claude_subscription" && !settings.claudeSubscriptionEnabled) continue;
478
+ claudeConnections[kind] = {
479
+ active:
480
+ kind === "claude_subscription"
481
+ ? claudePool.organization
482
+ : organizationProviders[kind].active,
483
+ models: organizationProviders[kind].models,
484
+ };
485
+ workspaceClaudeConnections[kind] = {
486
+ active:
487
+ kind === "claude_subscription" ? claudePool.workspace : workspaceConnectionActive(kind),
488
+ models: workspaceCustomModels[kind],
489
+ };
490
+ }
521
491
  return {
522
492
  claudeConnections,
523
493
  workspaceClaudeConnections,
@@ -527,14 +497,14 @@ export async function loadWorkspaceModelSelectionInput(
527
497
  codexSubscriptionActive,
528
498
  observations,
529
499
  xaiSubscriptionActive,
530
- workspaceGatewayConnectionActive,
531
- workspaceGatewayCustomModels,
532
- workspaceOpenRouterConnectionActive: openRouterConnectionActive,
533
- workspaceOpenRouterCustomModels,
534
- organizationGatewayConnectionActive,
535
- organizationOpenRouterConnectionActive,
536
- organizationGatewayCustomModels,
537
- organizationOpenRouterCustomModels,
500
+ workspaceGatewayConnectionActive: workspaceConnectionActive("vercel_gateway"),
501
+ workspaceGatewayCustomModels: workspaceCustomModels.vercel_gateway,
502
+ workspaceOpenRouterConnectionActive: workspaceConnectionActive("openrouter"),
503
+ workspaceOpenRouterCustomModels: workspaceCustomModels.openrouter,
504
+ organizationGatewayConnectionActive: organizationProviders.vercel_gateway.active,
505
+ organizationOpenRouterConnectionActive: organizationProviders.openrouter.active,
506
+ organizationGatewayCustomModels: organizationProviders.vercel_gateway.models,
507
+ organizationOpenRouterCustomModels: organizationProviders.openrouter.models,
538
508
  };
539
509
  }
540
510
 
@@ -3,7 +3,12 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
3
3
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { withLockedCapabilityInstallation } from "@opengeni/db/capability-reconciliation";
6
- import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
6
+ import {
7
+ environmentsEncryptionKeyBytes,
8
+ findIntegrationsOauthClient,
9
+ parseIntegrationsOauthClientsJson,
10
+ type Settings,
11
+ } from "@opengeni/config";
7
12
  import { pinnedFetch } from "@opengeni/network";
8
13
  import {
9
14
  CapabilityCatalogItem,
@@ -102,6 +107,7 @@ export async function buildCapabilityCatalog(input: {
102
107
  listEnabledMcpCapabilityServers(input.db, input.workspaceId),
103
108
  ]);
104
109
  const runnableCapabilityIds = new Set(runnableMcpServers.map((server) => server.capabilityId));
110
+ const operatorOAuthClientConfigured = operatorOAuthClientResolver(input.settings);
105
111
  const catalogInstallations = capabilityInstallations.filter(
106
112
  (installation) => installation.kind === "mcp",
107
113
  );
@@ -164,7 +170,9 @@ export async function buildCapabilityCatalog(input: {
164
170
  },
165
171
  }
166
172
  : projected;
167
- return applyCapabilityLifecycle(runtimeProjected);
173
+ return applyCapabilityLifecycle(
174
+ applyOperatorOAuthClientRequirement(runtimeProjected, operatorOAuthClientConfigured),
175
+ );
168
176
  })
169
177
  .sort(compareCatalogItems);
170
178
  return {
@@ -173,6 +181,40 @@ export async function buildCapabilityCatalog(input: {
173
181
  };
174
182
  }
175
183
 
184
+ /**
185
+ * `metadata.oauthClientRequirement` is stamped by the catalog import from
186
+ * `data/catalog/oauth-client-requirements.json` for providers that refuse
187
+ * OAuth self-registration. Whether the row can connect here is a deployment
188
+ * fact, so the catalog projects it rather than every surface guessing. The
189
+ * matcher is the one the OAuth start uses to find an operator client.
190
+ */
191
+ function operatorOAuthClientResolver(settings: Settings): (issuer: string) => boolean {
192
+ let configured: ReturnType<typeof parseIntegrationsOauthClientsJson> = {};
193
+ try {
194
+ configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
195
+ } catch {
196
+ // Startup validates this setting; a malformed value can configure nothing.
197
+ }
198
+ return (issuer) => findIntegrationsOauthClient(configured, [issuer]) !== null;
199
+ }
200
+
201
+ export function applyOperatorOAuthClientRequirement(
202
+ item: CapabilityCatalogItem,
203
+ configured: (issuer: string) => boolean,
204
+ ): CapabilityCatalogItem {
205
+ if (item.kind !== "mcp") return item;
206
+ const requirement = item.metadata.oauthClientRequirement;
207
+ const issuer =
208
+ requirement && typeof requirement === "object" && !Array.isArray(requirement)
209
+ ? (requirement as Record<string, unknown>).issuer
210
+ : undefined;
211
+ if (typeof issuer !== "string" || !issuer) return item;
212
+ return {
213
+ ...item,
214
+ runtime: { ...item.runtime, operatorOAuthClient: { configured: configured(issuer) } },
215
+ };
216
+ }
217
+
176
218
  export async function createCatalogItem(input: {
177
219
  db: Database;
178
220
  accountId: string;
@@ -23,7 +23,6 @@ import type {
23
23
  import {
24
24
  buildConnectionTokenResolver,
25
25
  getConnectionMetadata,
26
- listConnectionsMetadata,
27
26
  listEnabledMcpCapabilityServers,
28
27
  listConnectorToolPermissionPolicies,
29
28
  listChangedConnectorToolPermissions,
@@ -37,11 +36,16 @@ import {
37
36
  import { HTTPException } from "hono/http-exception";
38
37
  import {
39
38
  GMAIL_REST_MCP_TOOLS,
39
+ gmailToolAvailableOnDeployment,
40
40
  gmailToolSupportsScopes,
41
41
  isOfficialGmailMcpConfig,
42
42
  isOfficialSlackMcpConfig,
43
43
  } from "@opengeni/runtime";
44
44
  import { hasPermission } from "../access";
45
+ import {
46
+ personalConnectionDelegationSourceForGrant,
47
+ visibleMcpAccountConnections,
48
+ } from "./personal-connection-delegations";
45
49
  import {
46
50
  buildCapabilityCatalog,
47
51
  settingsWithMcpCapabilityServers,
@@ -195,12 +199,14 @@ async function resolveTarget(input: Input) {
195
199
  throw new HTTPException(403, {
196
200
  message: "Only the authenticated connection owner may manage personal tool permissions",
197
201
  });
202
+ const source = personalConnectionDelegationSourceForGrant(input.grant);
198
203
  const visible = ref
199
- ? await listConnectionsMetadata(
200
- input.db,
201
- input.workspaceId,
202
- input.personalOwnerVerified ? input.grant.subjectId : null,
203
- )
204
+ ? await visibleMcpAccountConnections(input.db, {
205
+ accountId: input.grant.accountId,
206
+ workspaceId: input.workspaceId,
207
+ source:
208
+ input.personalOwnerVerified && source.kind === "subject" ? source : { kind: "none" },
209
+ })
204
210
  : [];
205
211
  const bindings = mcpAccountBindingsFromVisibleConnections({
206
212
  accountId: input.grant.accountId,
@@ -256,9 +262,16 @@ async function resolveTarget(input: Input) {
256
262
  export function reviewedConnectorToolCatalog(
257
263
  server: Pick<Settings["mcpServers"][number], "url" | "connectionRef" | "allowedTools">,
258
264
  grantedScopes: readonly string[],
265
+ deployment: Pick<Settings, "gmailWatchTopicName">,
259
266
  ): ListedTool[] | null {
260
267
  const tools = isOfficialGmailMcpConfig(server.url, server.connectionRef)
261
- ? GMAIL_REST_MCP_TOOLS.filter((tool) => gmailToolSupportsScopes(tool.name, grantedScopes))
268
+ ? GMAIL_REST_MCP_TOOLS.filter(
269
+ (tool) =>
270
+ gmailToolSupportsScopes(tool.name, grantedScopes) &&
271
+ gmailToolAvailableOnDeployment(tool.name, {
272
+ watchTopicName: deployment.gmailWatchTopicName,
273
+ }),
274
+ )
262
275
  : isOfficialSlackMcpConfig(server.url, server.connectionRef)
263
276
  ? slackRestMcpToolsForScopes(grantedScopes)
264
277
  : null;
@@ -275,6 +288,7 @@ async function listTools(
275
288
  const reviewed = reviewedConnectorToolCatalog(
276
289
  target.server,
277
290
  target.connection?.grantedScopes ?? [],
291
+ input.settings,
278
292
  );
279
293
  if (reviewed !== null) return reviewed;
280
294
  let headers = { ...target.server.headers };
@@ -283,7 +297,9 @@ async function listTools(
283
297
  input.db,
284
298
  input.settings,
285
299
  )({
286
- workspaceId: input.workspaceId,
300
+ // The selected account's credentials remain in its origin workspace;
301
+ // approval preferences remain scoped to the workspace being configured.
302
+ workspaceId: target.connection?.workspaceId ?? input.workspaceId,
287
303
  ...(target.connection?.subjectId ? { subjectId: input.grant.subjectId } : {}),
288
304
  serverId: target.server.id,
289
305
  toolName: "tools/list",