@opengeni/core 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/application/connect-operation.d.ts +3 -0
  3. package/dist/application/sandbox-recovery.d.ts +49 -0
  4. package/dist/{chunk-H7X52RI2.js → chunk-TFHSE7XR.js} +13 -2
  5. package/dist/chunk-TFHSE7XR.js.map +1 -0
  6. package/dist/dependencies.d.ts +1 -1
  7. package/dist/domain/capabilities.d.ts +2 -2
  8. package/dist/domain/connection-account-selection-error.d.ts +6 -0
  9. package/dist/domain/connector-tool-permissions.d.ts +27 -0
  10. package/dist/domain/editable-artifacts/agent-application.d.ts +8 -0
  11. package/dist/domain/environments.d.ts +3 -7
  12. package/dist/domain/insights-timing.d.ts +16 -0
  13. package/dist/domain/insights.d.ts +4 -1
  14. package/dist/domain/knowledge-files.d.ts +1 -1
  15. package/dist/domain/knowledge-preparation.d.ts +9 -0
  16. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  17. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  18. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  19. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  20. package/dist/domain/personal-connection-delegations.d.ts +19 -10
  21. package/dist/domain/pr-review.d.ts +56 -4
  22. package/dist/domain/scheduled-tasks.d.ts +8 -5
  23. package/dist/domain/session-file-access.d.ts +4 -0
  24. package/dist/domain/sessions.d.ts +8 -6
  25. package/dist/domain/skills.d.ts +29 -1
  26. package/dist/editable-artifacts.js +1 -1
  27. package/dist/index.d.ts +7 -5
  28. package/dist/index.js +4868 -4803
  29. package/dist/index.js.map +1 -1
  30. package/dist/sandbox/routing.d.ts +2 -1
  31. package/dist/sandbox/runtime-settings.d.ts +1 -15
  32. package/package.json +11 -19
  33. package/src/application/composer-submit.ts +2 -1
  34. package/src/application/connect-operation.ts +32 -3
  35. package/src/application/sandbox-recovery.ts +55 -0
  36. package/src/application/user-resource-grants.ts +1 -16
  37. package/src/dependencies.ts +5 -1
  38. package/src/domain/capabilities.ts +169 -122
  39. package/src/domain/connection-account-selection-error.ts +9 -0
  40. package/src/domain/connector-tool-permissions.ts +305 -0
  41. package/src/domain/editable-artifacts/agent-application.ts +31 -2
  42. package/src/domain/environments.ts +4 -9
  43. package/src/domain/file-owner.ts +16 -1
  44. package/src/domain/insights-timing.ts +95 -0
  45. package/src/domain/insights.ts +59 -35
  46. package/src/domain/knowledge-files.ts +10 -3
  47. package/src/domain/knowledge-messages.ts +1 -0
  48. package/src/domain/knowledge-preparation.test.ts +251 -0
  49. package/src/domain/knowledge-preparation.ts +126 -0
  50. package/src/domain/mcp-account-bindings.ts +152 -0
  51. package/src/domain/native-mcp-connection-admission.ts +14 -0
  52. package/src/domain/organization-integration-catalog.ts +83 -0
  53. package/src/domain/personal-connection-delegations.ts +226 -177
  54. package/src/domain/pr-review.ts +22 -6
  55. package/src/domain/resources.ts +9 -2
  56. package/src/domain/scheduled-tasks.ts +200 -155
  57. package/src/domain/session-file-access.ts +47 -0
  58. package/src/domain/session-tool-policy.ts +12 -4
  59. package/src/domain/sessions.ts +390 -261
  60. package/src/domain/skills.ts +9 -0
  61. package/src/index.ts +10 -6
  62. package/src/rigs/index.ts +17 -17
  63. package/src/sandbox/routing.ts +18 -7
  64. package/src/sandbox/runtime-settings.ts +7 -91
  65. package/dist/application/host-mcp-owner.d.ts +0 -6
  66. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  67. package/dist/chunk-5WZYFL45.js +0 -266
  68. package/dist/chunk-5WZYFL45.js.map +0 -1
  69. package/dist/chunk-H7X52RI2.js.map +0 -1
  70. package/dist/domain/connection-authority.d.ts +0 -97
  71. package/dist/domain/connection-authority.js +0 -169
  72. package/dist/domain/connection-authority.js.map +0 -1
  73. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  74. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  75. package/dist/domain/packs.d.ts +0 -59
  76. package/dist/domain/product-integration-pack.d.ts +0 -44
  77. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  78. package/dist/remote-mcp-credentials.d.ts +0 -23
  79. package/dist/remote-mcp-credentials.js +0 -11
  80. package/dist/remote-mcp-credentials.js.map +0 -1
  81. package/src/application/host-mcp-owner.ts +0 -41
  82. package/src/application/host-mcp-resolvers.ts +0 -84
  83. package/src/domain/connection-authority.ts +0 -329
  84. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  85. package/src/domain/host-mcp-task-admission.ts +0 -121
  86. package/src/domain/packs.ts +0 -699
  87. package/src/domain/product-integration-pack.ts +0 -42
  88. package/src/domain/product-integration-skill.gen.ts +0 -52
  89. package/src/remote-mcp-credentials.ts +0 -361
@@ -1,6 +1,8 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
3
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { withLockedCapabilityInstallation } from "@opengeni/db/capability-reconciliation";
4
6
  import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
5
7
  import {
6
8
  CapabilityCatalogItem,
@@ -16,7 +18,7 @@ import {
16
18
  type EnableCapabilityRequest,
17
19
  type McpServerConnectionRef,
18
20
  type McpPersonalConnectionDelegation,
19
- OPENGENI_PERSONAL_SLACK_MCP_URL,
21
+ assertOrganizationIntegrationAllowed,
20
22
  type SocialConnection,
21
23
  } from "@opengeni/contracts";
22
24
  import {
@@ -43,7 +45,6 @@ import {
43
45
  listEnabledMcpCapabilityServers,
44
46
  listInstalledApiIntegrations,
45
47
  listInstalledSkills,
46
- listPackInstallations,
47
48
  listSocialConnections,
48
49
  mcpServerIdForCapability,
49
50
  upsertCapabilityCatalogItem,
@@ -53,11 +54,14 @@ import {
53
54
  type InstalledSkillSummary,
54
55
  } from "@opengeni/db";
55
56
  import { HTTPException } from "hono/http-exception";
57
+ import {
58
+ withOrganizationIntegrationAcquisition,
59
+ withOrganizationIntegrationPolicyFence,
60
+ } from "@opengeni/db/organization-integration-policy";
56
61
  import { hasPermission } from "../access";
57
62
  import { isFikenConnection, preferredFikenConnection } from "./fiken";
58
63
  import { listSkillLibraryEntries, type SkillLibraryEntry } from "@opengeni/runtime/skill-library";
59
- import { listCapabilityPacks, listWorkspaceCapabilityPacks } from "./packs";
60
- import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
64
+ import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
61
65
 
62
66
  const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
63
67
  const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
@@ -66,7 +70,6 @@ const mcpRegistryMaxPages = 3;
66
70
  const mcpCapabilityProbeTimeoutMs = 15000;
67
71
  const maxMcpCredentialHeaders = 16;
68
72
  const maxMcpCredentialHeaderValueLength = 4096;
69
- const officialGmailMcpUrl = "https://gmailmcp.googleapis.com/mcp/v1";
70
73
  // RFC 9110 field-name token characters.
71
74
  const mcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
72
75
 
@@ -79,18 +82,15 @@ export async function buildCapabilityCatalog(input: {
79
82
  const [
80
83
  persistedItems,
81
84
  capabilityInstallations,
82
- packInstallations,
83
- workspacePacks,
84
85
  socialConnections,
85
86
  workspaceConnections,
86
87
  curatedLibrarySkills,
87
88
  installedSkills,
88
89
  codexAppsCredentialId,
90
+ runnableMcpServers,
89
91
  ] = await Promise.all([
90
92
  listCapabilityCatalogItems(input.db, input.workspaceId),
91
93
  listCapabilityInstallations(input.db, input.workspaceId),
92
- listPackInstallations(input.db, input.workspaceId),
93
- listWorkspaceCapabilityPacks(input.db, input.workspaceId),
94
94
  listSocialConnections(input.db, input.workspaceId, 500, input.subjectId),
95
95
  listConnectionsMetadata(input.db, input.workspaceId, null),
96
96
  discoverCuratedSkillLibraryItems(),
@@ -98,7 +98,9 @@ export async function buildCapabilityCatalog(input: {
98
98
  input.settings.codexConnectedAppsEnabled
99
99
  ? resolveCodexAppsCredentialIdForRun(input.db, input.workspaceId)
100
100
  : Promise.resolve(null),
101
+ listEnabledMcpCapabilityServers(input.db, input.workspaceId),
101
102
  ]);
103
+ const runnableCapabilityIds = new Set(runnableMcpServers.map((server) => server.capabilityId));
102
104
  const catalogInstallations = capabilityInstallations.filter(
103
105
  (installation) => installation.kind === "mcp",
104
106
  );
@@ -110,16 +112,7 @@ export async function buildCapabilityCatalog(input: {
110
112
  .filter((skill) => skill.owners.some((owner) => owner.kind === "direct"))
111
113
  .map((skill) => [skill.capabilityId, skill]),
112
114
  );
113
- const activePackIds = new Set(
114
- packInstallations
115
- .filter((installation) => installation.status === "active")
116
- .map((installation) => installation.packId),
117
- );
118
- const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
119
115
  const builtIns = [
120
- ...workspacePacks.map((pack) =>
121
- packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual"),
122
- ),
123
116
  ...configuredMcpCatalogItems(input.settings),
124
117
  ...providerIntegrationCatalogItems(socialConnections),
125
118
  fikenCatalogItem(workspaceConnections.filter(isFikenConnection)),
@@ -151,8 +144,26 @@ export async function buildCapabilityCatalog(input: {
151
144
  const projected =
152
145
  item.kind === "skill"
153
146
  ? applyInstalledSkillEnablement(item, installedSkillById.get(item.id))
154
- : applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds);
155
- return applyCapabilityLifecycle(projected);
147
+ : applyCapabilityEnablement(item, capabilityInstallationById.get(item.id));
148
+ // An installed connector can become unrunnable after a lifecycle or
149
+ // ownership change. Use the execution registry rather than presenting
150
+ // its stale catalog definition as a selectable tool.
151
+ const runtimeProjected =
152
+ projected.kind === "mcp" &&
153
+ projected.enabled &&
154
+ projected.source !== "configured" &&
155
+ projected.source !== "built_in" &&
156
+ !runnableCapabilityIds.has(projected.id)
157
+ ? {
158
+ ...projected,
159
+ runtime: {
160
+ ...projected.runtime,
161
+ available: false,
162
+ notes: "This installed connection is unavailable. Review its connection settings.",
163
+ },
164
+ }
165
+ : projected;
166
+ return applyCapabilityLifecycle(runtimeProjected);
156
167
  })
157
168
  .sort(compareCatalogItems);
158
169
  return {
@@ -168,11 +179,7 @@ export async function createCatalogItem(input: {
168
179
  payload: CreateCapabilityCatalogItemRequest;
169
180
  }): Promise<CapabilityCatalogItem> {
170
181
  const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
171
- if (id.startsWith("pack:")) {
172
- throw new HTTPException(422, {
173
- message: "packs are managed by OpenGeni and cannot be manually created",
174
- });
175
- }
182
+
176
183
  if (id.startsWith("skill:")) {
177
184
  throw new HTTPException(422, {
178
185
  message: "Skills are installed through the Skill library or source import flow",
@@ -252,6 +259,7 @@ export async function enableCapability(
252
259
  /** Probe outside a durable Connect commit; persist the exact prepared settings
253
260
  * inside the caller's authorized receipt transaction. Native enable uses this too. */
254
261
  export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
262
+ input = { ...input, payload: structuredClone(input.payload) };
255
263
  const item = await requireCatalogItem(
256
264
  input.db,
257
265
  input.workspaceId,
@@ -277,11 +285,7 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
277
285
  message: "Install Plugins through the Plugin Package flow",
278
286
  });
279
287
  }
280
- if (item.kind === "pack") {
281
- throw new HTTPException(409, {
282
- message: "Install Packs through the Pack installation preview flow",
283
- });
284
- }
288
+
285
289
  if (item.kind === "mcp" && !item.runtime.available) {
286
290
  throw new HTTPException(422, {
287
291
  message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
@@ -297,6 +301,40 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
297
301
  delete installationConfig.headerNames;
298
302
  delete installationConfig.connectionRef;
299
303
  if (item.kind === "mcp") {
304
+ // This generic, potentially workspace-authored catalog is not proof of a
305
+ // curated provider identity. Dedicated Connect adapters classify separately.
306
+ const unchanged = await withOrganizationIntegrationPolicyFence(
307
+ input.db,
308
+ input,
309
+ async (tx, policy) => {
310
+ const existing = await unchangedMcpInstallation(
311
+ { ...input, db: tx },
312
+ item,
313
+ installationConfig,
314
+ );
315
+ if (existing) return existing;
316
+ assertOrganizationIntegrationAllowed(policy, "custom:mcp");
317
+ return null;
318
+ },
319
+ );
320
+ if (unchanged) {
321
+ return {
322
+ commit: (db: Database) =>
323
+ withOrganizationIntegrationPolicyFence(db, input, async (tx) => {
324
+ const current = await unchangedMcpInstallation(
325
+ { ...input, db: tx },
326
+ item,
327
+ installationConfig,
328
+ );
329
+ if (!current || !isDeepStrictEqual(current, unchanged)) {
330
+ throw new HTTPException(409, {
331
+ message: "Capability installation changed; reload before reconciling",
332
+ });
333
+ }
334
+ return current;
335
+ }),
336
+ };
337
+ }
300
338
  const headers = await resolveMcpCredentialHeaders(input, item);
301
339
  const connectionRef = input.payload.connectionRef
302
340
  ? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef)
@@ -325,8 +363,59 @@ export async function prepareCapabilityEnable(input: EnableCapabilityInput) {
325
363
  kind: item.kind,
326
364
  config: installationConfig,
327
365
  metadata: installationMetadata,
366
+ ...(input.payload.onlyIfUninstalled ? { onlyIfUninstalled: true } : {}),
367
+ };
368
+ return {
369
+ commit: (db: Database) =>
370
+ item.kind === "mcp"
371
+ ? withOrganizationIntegrationAcquisition(db, installation, ["custom:mcp"], (tx) =>
372
+ enableCapabilityInstallation(tx, installation),
373
+ )
374
+ : enableCapabilityInstallation(db, installation),
328
375
  };
329
- return { commit: (db: Database) => enableCapabilityInstallation(db, installation) };
376
+ }
377
+
378
+ /** A no-effect reconciliation is not a new acquisition. Validate ordinary
379
+ * connection visibility before locking the installation, and compare actual
380
+ * stored credentials rather than trusting caller-supplied identity hints. */
381
+ async function unchangedMcpInstallation(
382
+ input: EnableCapabilityInput,
383
+ item: CapabilityCatalogItem,
384
+ requestedConfig: Record<string, unknown>,
385
+ ): Promise<CapabilityInstallation | null> {
386
+ const connectionRef = input.payload.connectionRef
387
+ ? await validateMcpCapabilityConnectionRef(input, item, input.payload.connectionRef)
388
+ : null;
389
+ const existing = await withLockedCapabilityInstallation(
390
+ input.db,
391
+ input.workspaceId,
392
+ item.id,
393
+ (tx) => getCapabilityInstallation(tx, input.workspaceId, item.id),
394
+ );
395
+ if (!existing || existing.kind !== "mcp" || existing.status !== "active") return null;
396
+ const {
397
+ headerNames: _names,
398
+ headersEncrypted: _encrypted,
399
+ headers: _headers,
400
+ ...storedConfig
401
+ } = existing.config;
402
+ const config = { ...requestedConfig, ...(connectionRef ? { connectionRef } : {}) };
403
+ const { mcpConnectivity: _storedConnectivity, ...storedMetadata } = existing.metadata;
404
+ const { mcpConnectivity: _requestedConnectivity, ...requestedMetadata } = input.payload.metadata;
405
+ if (
406
+ !isDeepStrictEqual(config, storedConfig) ||
407
+ !isDeepStrictEqual(requestedMetadata, storedMetadata)
408
+ )
409
+ return null;
410
+ const provided = normalizedMcpCredentialHeaders(input.payload.headers);
411
+ if (provided) {
412
+ const stored = await resolveMcpCredentialHeaders(
413
+ { ...input, payload: { ...input.payload, headers: {} } },
414
+ item,
415
+ );
416
+ if (!isDeepStrictEqual(provided, stored)) return null;
417
+ }
418
+ return existing;
330
419
  }
331
420
 
332
421
  /**
@@ -425,24 +514,11 @@ async function validateMcpCapabilityConnectionRef(
425
514
  ref: McpServerConnectionRef,
426
515
  ): Promise<McpServerConnectionRef> {
427
516
  const subjectScope = ref.subjectScope ?? "workspace";
428
- const endpointUrl = item.endpointUrl?.replace(/\/+$/, "");
429
- // Gmail is a consumer mailbox; Slack's hosted MCP issues user tokens only and
430
- // shared Slack authority belongs to the OpenGeni workspace bot. Neither may
431
- // become a workspace-owned connection reference.
432
- const personalOnly =
433
- item.metadata.connectionOwnership === "personal_only" ||
434
- endpointUrl === officialGmailMcpUrl ||
435
- endpointUrl === OPENGENI_PERSONAL_SLACK_MCP_URL;
436
- if (personalOnly && subjectScope !== "subject") {
437
- throw new HTTPException(422, {
438
- message:
439
- "this capability requires a personal connection; each workspace member must connect their own account",
440
- });
441
- }
442
517
  const normalized: McpServerConnectionRef = {
443
518
  providerDomain: ref.providerDomain.trim(),
444
519
  subjectScope,
445
520
  ...(ref.connectionId ? { connectionId: ref.connectionId } : {}),
521
+ ...(ref.accountSelection ? { accountSelection: ref.accountSelection } : {}),
446
522
  ...(ref.authoritySource === "host" ? { authoritySource: "host" as const } : {}),
447
523
  ...(ref.provider ? { provider: ref.provider.trim() } : {}),
448
524
  ...(ref.kind ? { kind: ref.kind } : {}),
@@ -468,7 +544,7 @@ async function validateMcpCapabilityConnectionRef(
468
544
  });
469
545
  }
470
546
  if (normalized.authoritySource === "host") {
471
- assertHostMcpAuthoritySourceAdmissionEnabled(input.settings, normalized);
547
+ assertNativeMcpConnectionRef(normalized);
472
548
  return normalized;
473
549
  }
474
550
 
@@ -480,7 +556,11 @@ async function validateMcpCapabilityConnectionRef(
480
556
  input.grant.subjectId,
481
557
  )
482
558
  : null;
483
- if (!connection && subjectScope === "subject" && !normalized.connectionId) {
559
+ if (
560
+ !connection &&
561
+ !normalized.connectionId &&
562
+ (subjectScope === "subject" || normalized.accountSelection === "all_eligible")
563
+ ) {
484
564
  const visible = await listConnectionsMetadata(
485
565
  input.db,
486
566
  input.workspaceId,
@@ -489,7 +569,7 @@ async function validateMcpCapabilityConnectionRef(
489
569
  connection =
490
570
  visible.find(
491
571
  (candidate) =>
492
- candidate.subjectId === input.grant.subjectId &&
572
+ candidate.subjectId === (subjectScope === "subject" ? input.grant.subjectId : null) &&
493
573
  candidate.providerDomain === normalized.providerDomain &&
494
574
  (!normalized.kind || candidate.kind === normalized.kind) &&
495
575
  candidate.status === "active",
@@ -715,11 +795,7 @@ export async function disableCapability(input: {
715
795
  message: "Remove Plugins through the Plugin Package flow",
716
796
  });
717
797
  }
718
- if (item.kind === "pack") {
719
- throw new HTTPException(409, {
720
- message: "Uninstall Packs through the Pack uninstall preview flow",
721
- });
722
- }
798
+
723
799
  if (item.source === "built_in" || item.source === "configured") {
724
800
  throw new HTTPException(409, {
725
801
  message:
@@ -773,7 +849,7 @@ export function apiIntegrationsMatchingDelegations(
773
849
  const exact = new Set(
774
850
  delegations.map((delegation) =>
775
851
  [
776
- delegation.serverId,
852
+ delegation.canonicalServerId ?? delegation.serverId,
777
853
  delegation.connectionId,
778
854
  delegation.providerDomain.toLowerCase(),
779
855
  delegation.kind ?? "",
@@ -1055,52 +1131,6 @@ async function requireCatalogItem(
1055
1131
  return item;
1056
1132
  }
1057
1133
 
1058
- function packCatalogItem(
1059
- pack: ReturnType<typeof listCapabilityPacks>[number],
1060
- source: "built_in" | "manual",
1061
- ): CapabilityCatalogItem {
1062
- const customMetadata = { ...pack.metadata };
1063
- for (const key of [
1064
- "packId",
1065
- "version",
1066
- "connectors",
1067
- "knowledge",
1068
- "scheduledTaskTemplates",
1069
- "sandboxImage",
1070
- "sandboxProviderImages",
1071
- "skills",
1072
- ]) {
1073
- delete customMetadata[key];
1074
- }
1075
- return CapabilityCatalogItem.parse({
1076
- id: `pack:${pack.id}`,
1077
- kind: "pack",
1078
- source,
1079
- name: pack.name,
1080
- description: pack.description,
1081
- category: pack.category,
1082
- tags: [pack.role, pack.category, "pack"],
1083
- tools: pack.tools,
1084
- runtime: {
1085
- available: true,
1086
- notes: "Enables role-scoped tools, connectors, knowledge, and scheduled-task templates.",
1087
- },
1088
- metadata: {
1089
- ...customMetadata,
1090
- packId: pack.id,
1091
- version: pack.version,
1092
- connectors: pack.connectors,
1093
- knowledge: pack.knowledge,
1094
- scheduledTaskTemplates: pack.scheduledTaskTemplates,
1095
- ...(pack.variableSet ? { variableSet: pack.variableSet } : {}),
1096
- // Runtime composition surface only: skill names, never file content.
1097
- ...(pack.sandboxImage ? { sandboxImage: pack.sandboxImage } : {}),
1098
- ...(pack.sandboxProviderImages ? { sandboxProviderImages: pack.sandboxProviderImages } : {}),
1099
- ...(pack.skills.length > 0 ? { skills: pack.skills.map((skill) => skill.name) } : {}),
1100
- },
1101
- });
1102
- }
1103
-
1104
1134
  function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
1105
1135
  return (
1106
1136
  settings.mcpServers
@@ -1121,6 +1151,17 @@ function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[]
1121
1151
  category: "configured",
1122
1152
  tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
1123
1153
  endpointUrl: server.url,
1154
+ authKind:
1155
+ server.connectionRef?.authoritySource !== "host" &&
1156
+ server.connectionRef?.kind === "oauth2"
1157
+ ? "oauth2"
1158
+ : null,
1159
+ // Deployment-managed personal selectors need the same account picker
1160
+ // as installed connectors. Fixed bindings remain server-resolved.
1161
+ connectionRef:
1162
+ server.connectionRef?.subjectScope === "subject" && !server.connectionRef.connectionId
1163
+ ? installationConnectionRef({ connectionRef: server.connectionRef })
1164
+ : null,
1124
1165
  tools: [{ kind: "mcp", id: server.id }],
1125
1166
  runtime: {
1126
1167
  available: true,
@@ -1521,19 +1562,10 @@ function socialProviderConnectionCounts(item: CapabilityCatalogItem): {
1521
1562
  export function applyCapabilityEnablement(
1522
1563
  item: CapabilityCatalogItem,
1523
1564
  installation: CapabilityInstallation | undefined,
1524
- activePackIds: Set<string>,
1525
1565
  ): CapabilityCatalogItem {
1526
1566
  if (item.kind === "skill" || item.kind === "api" || item.kind === "plugin") {
1527
1567
  return { ...item, enabled: false, enabledReason: null, connectionRef: null };
1528
1568
  }
1529
- if (item.kind === "pack") {
1530
- const enabled = activePackIds.has(packIdFromCapabilityId(item.id));
1531
- return {
1532
- ...item,
1533
- enabled,
1534
- enabledReason: enabled ? "enabled" : null,
1535
- };
1536
- }
1537
1569
  if (isSocialProviderIntegration(item)) {
1538
1570
  // Provider-integration state is derived from every authoritative visible
1539
1571
  // social Connection while the catalog is built. The catalog summary never
@@ -1557,7 +1589,7 @@ export function applyCapabilityEnablement(
1557
1589
  ...item,
1558
1590
  enabled: true,
1559
1591
  enabledReason: "managed by deployment",
1560
- connectionRef: null,
1592
+ connectionRef: item.connectionRef,
1561
1593
  };
1562
1594
  }
1563
1595
  if (item.source === "built_in") {
@@ -1568,11 +1600,17 @@ export function applyCapabilityEnablement(
1568
1600
  }
1569
1601
  const activeInstallation = installation?.status === "active";
1570
1602
  const enabled = !!activeInstallation && capabilityInstallationRuntimeReady(item, installation);
1603
+ const connectionRef =
1604
+ enabled && installation ? installationConnectionRef(installation.config) : null;
1571
1605
  return {
1572
1606
  ...item,
1607
+ // Older custom entries recorded the native credential kind on the
1608
+ // installation only. Project it without guessing from a URL or overwriting
1609
+ // an explicit catalog authentication contract.
1610
+ authKind: item.authKind ?? (connectionRef?.kind === "oauth2" ? "oauth2" : null),
1573
1611
  enabled,
1574
1612
  enabledReason: enabled ? "enabled" : null,
1575
- connectionRef: enabled && installation ? installationConnectionRef(installation.config) : null,
1613
+ connectionRef,
1576
1614
  };
1577
1615
  }
1578
1616
 
@@ -1599,7 +1637,10 @@ function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalo
1599
1637
  detail: item.runtime.notes,
1600
1638
  managedBy: item.source === "built_in" ? "platform" : null,
1601
1639
  },
1602
- actions: ["inspect"],
1640
+ actions:
1641
+ item.enabled && item.kind === "mcp" && item.source !== "built_in"
1642
+ ? ["disconnect", "inspect"]
1643
+ : ["inspect"],
1603
1644
  };
1604
1645
  }
1605
1646
 
@@ -1638,7 +1679,7 @@ function applyCapabilityLifecycle(item: CapabilityCatalogItem): CapabilityCatalo
1638
1679
 
1639
1680
  const installed = item.enabled;
1640
1681
  const actions: CapabilityAction[] = installed
1641
- ? item.kind === "pack" || item.kind === "skill" || item.kind === "plugin"
1682
+ ? item.kind === "skill" || item.kind === "plugin"
1642
1683
  ? ["configure", "update", "uninstall", "inspect"]
1643
1684
  : ["configure", "disconnect", "inspect"]
1644
1685
  : item.kind === "mcp" || item.kind === "api"
@@ -1701,10 +1742,8 @@ function installationConnectionRef(
1701
1742
  if (!ref || typeof ref !== "object") {
1702
1743
  return null;
1703
1744
  }
1704
- const { authoritySource, connectionId, providerDomain, kind, subjectScope } = ref as Record<
1705
- string,
1706
- unknown
1707
- >;
1745
+ const { authoritySource, connectionId, providerDomain, kind, subjectScope, accountSelection } =
1746
+ ref as Record<string, unknown>;
1708
1747
  if (typeof providerDomain !== "string" || typeof kind !== "string") {
1709
1748
  return null;
1710
1749
  }
@@ -1715,10 +1754,22 @@ function installationConnectionRef(
1715
1754
  // open old browser bundles cannot treat a host UUID as native OAuth state.
1716
1755
  return null;
1717
1756
  }
1757
+ if (accountSelection === "all_eligible" && connectionId === undefined) {
1758
+ return {
1759
+ providerDomain,
1760
+ kind,
1761
+ accountSelection,
1762
+ ...(subjectScope === "subject" ? { subjectScope } : {}),
1763
+ };
1764
+ }
1718
1765
  if (subjectScope === "subject") {
1719
1766
  // Never project a native personal connection UUID through workspace-visible
1720
1767
  // capability configuration, including legacy rows that still contain one.
1721
- return { providerDomain, kind, subjectScope: "subject" };
1768
+ return {
1769
+ providerDomain,
1770
+ kind,
1771
+ subjectScope: "subject",
1772
+ };
1722
1773
  }
1723
1774
  if (typeof connectionId !== "string") {
1724
1775
  return null;
@@ -1844,10 +1895,6 @@ function publicRegistryCapabilityId(name: string, version: string, endpointUrl:
1844
1895
  return `mcp-registry:${slugify(name)}-${shortHash(`${name}:${version}:${endpointUrl}`)}`;
1845
1896
  }
1846
1897
 
1847
- function packIdFromCapabilityId(capabilityId: string): string {
1848
- return capabilityId.replace(/^pack:/, "");
1849
- }
1850
-
1851
1898
  function uniqueTags(tags: string[]): string[] {
1852
1899
  return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
1853
1900
  }
@@ -0,0 +1,9 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+
3
+ /** An account choice needs attention; retrying the same work cannot fix it. */
4
+ export class ConnectionAccountSelectionError extends HTTPException {
5
+ override name = "ConnectionAccountSelectionError";
6
+ constructor(message: string) {
7
+ super(422, { message });
8
+ }
9
+ }