@opengeni/core 3.0.1 → 3.1.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 (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -1,8 +1,10 @@
1
1
  import type { McpServerConfig, Settings } from "@opengeni/config";
2
2
  import type {
3
3
  AccessGrant,
4
+ ConnectionAccountIntent,
4
5
  ConnectionMetadata,
5
- McpConnectionAuthoritySelection,
6
+ McpConnectionAccountBinding,
7
+ McpConnectionAccountSelection,
6
8
  McpPersonalConnectionDelegation,
7
9
  McpServerConnectionRef,
8
10
  ResourceRef,
@@ -24,17 +26,25 @@ import {
24
26
  import {
25
27
  getPersonalGitHubRepositorySelectionState,
26
28
  getSessionTurnPersonalConnectionDelegations,
29
+ getSessionTurnMcpAccountBindings,
30
+ getSessionTurnConnectionAccountIntent,
27
31
  getConnectionMetadata,
28
32
  getSocialConnection,
29
33
  listConnectionsMetadata,
30
34
  listSocialConnections,
31
- resolvePersonalConnectionAuthoritySelectionOrigin,
35
+ listOwnedConnectionAccounts,
32
36
  namedSubjectHasLiveWorkspaceAuthority,
33
37
  type Database,
34
38
  type ResolveConnectionCredentialInput,
35
39
  type ResolveConnectionCredentialResult,
36
40
  } from "@opengeni/db";
37
41
  import { personalGitHubRepositoryResources } from "./resources";
42
+ import { ConnectionAccountSelectionError } from "./connection-account-selection-error";
43
+ export { ConnectionAccountSelectionError } from "./connection-account-selection-error";
44
+ import {
45
+ mcpAccountBindingsFromVisibleConnections,
46
+ personalDelegationsForAccountBindings,
47
+ } from "./mcp-account-bindings";
38
48
 
39
49
  export type PersonalConnectionDelegationSource =
40
50
  | {
@@ -157,6 +167,67 @@ export function personalConnectionDelegationSourceForGrant(
157
167
  return { kind: "subject", subjectId: grant.subjectId, accountId: grant.accountId };
158
168
  }
159
169
 
170
+ /** Account-picker inventory: shared workspace accounts and this sender's own
171
+ * personal accounts. Visibility is never an execution grant or a credential
172
+ * response; every selected connection is validated again at admission/use. */
173
+ export async function listOwnConnectionAccountsForGrant(
174
+ db: Database,
175
+ grant: AccessGrant,
176
+ options: { includeInactive?: boolean } = {},
177
+ ): Promise<ConnectionMetadata[]> {
178
+ const source = personalConnectionDelegationSourceForGrant(grant);
179
+ const workspace = (await listConnectionsMetadata(db, grant.workspaceId, null)).filter(
180
+ (connection) =>
181
+ connection.subjectId === null &&
182
+ connection.workspaceId === grant.workspaceId &&
183
+ connection.accountId === grant.accountId &&
184
+ (options.includeInactive === true || connection.status === "active"),
185
+ );
186
+ if (source.kind !== "subject") return workspace;
187
+ const personal = await listOwnConnectionMetadata(db, {
188
+ ...source,
189
+ workspaceId: grant.workspaceId,
190
+ includeInactive: options.includeInactive === true,
191
+ });
192
+ return [...workspace, ...personal];
193
+ }
194
+
195
+ async function listOwnConnectionMetadata(
196
+ db: Database,
197
+ input: {
198
+ accountId: string;
199
+ workspaceId: string;
200
+ subjectId: string;
201
+ includeInactive?: boolean;
202
+ /** Read only these owned accounts (a live target fence); omit for all. */
203
+ connectionIds?: ReadonlySet<string>;
204
+ },
205
+ ): Promise<ConnectionMetadata[]> {
206
+ const accounts = (await listOwnedConnectionAccounts(db, input)).filter(
207
+ (account) => !input.connectionIds || input.connectionIds.has(account.connectionId),
208
+ );
209
+ const connections: ConnectionMetadata[] = [];
210
+ // The caller may supply a transaction handle (for example, credential
211
+ // rotation). Each metadata read opens a nested RLS savepoint on that same
212
+ // connection, so these reads must not overlap.
213
+ for (const account of accounts) {
214
+ const connection = await getConnectionMetadata(
215
+ db,
216
+ account.originWorkspaceId,
217
+ account.connectionId,
218
+ input.subjectId,
219
+ );
220
+ if (
221
+ connection?.accountId === input.accountId &&
222
+ connection.subjectId === input.subjectId &&
223
+ connection.authorityId != null &&
224
+ (input.includeInactive === true || connection.status === "active")
225
+ )
226
+ connections.push(connection);
227
+ }
228
+ return connections;
229
+ }
230
+
160
231
  export async function authorizedSocialConnectionsForGrant(input: {
161
232
  db: Database;
162
233
  grant: AccessGrant;
@@ -215,74 +286,12 @@ export async function authorizedSocialConnectionsForGrant(input: {
215
286
  return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
216
287
  }
217
288
 
218
- export async function authorizedAtlassianConnectionsForGrant(input: {
289
+ /** Historical native Atlassian grants no longer authorize API execution. */
290
+ export async function authorizedAtlassianConnectionsForGrant(_input: {
219
291
  db: Database;
220
292
  grant: AccessGrant;
221
293
  }): Promise<AuthorizedAtlassianConnection[]> {
222
- const workspace = (await listConnectionsMetadata(input.db, input.grant.workspaceId, null)).filter(
223
- (connection) =>
224
- connection.subjectId === null &&
225
- connection.status === "active" &&
226
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
227
- );
228
- const source = personalConnectionDelegationSourceForGrant(input.grant);
229
- if (source.kind === "none") {
230
- return workspace.map((connection) => ({ connection, subjectId: null }));
231
- }
232
- if (source.kind === "subject") {
233
- const visible = await listConnectionsMetadata(
234
- input.db,
235
- input.grant.workspaceId,
236
- source.subjectId,
237
- );
238
- return visible
239
- .filter(
240
- (connection) =>
241
- connection.status === "active" &&
242
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
243
- )
244
- .map((connection) => ({
245
- connection,
246
- subjectId: connection.subjectId === null ? null : source.subjectId,
247
- }));
248
- }
249
- const delegations = (
250
- await getSessionTurnPersonalConnectionDelegations(
251
- input.db,
252
- input.grant.workspaceId,
253
- source.sessionId,
254
- source.turnId,
255
- )
256
- ).filter((item) => item.connectionType === "atlassian");
257
- const personal: AuthorizedAtlassianConnection[] = [];
258
- for (const delegation of delegations) {
259
- if (
260
- !(await ownerStillBelongsToWorkspace(input.db, {
261
- accountId: input.grant.accountId,
262
- workspaceId: input.grant.workspaceId,
263
- subjectId: delegation.ownerSubjectId,
264
- }))
265
- ) {
266
- continue;
267
- }
268
- const connection = await getConnectionMetadata(
269
- input.db,
270
- input.grant.workspaceId,
271
- delegation.connectionId,
272
- delegation.ownerSubjectId,
273
- );
274
- if (
275
- !connection ||
276
- connection.subjectId !== delegation.ownerSubjectId ||
277
- connection.status !== "active" ||
278
- !sameProviderDomain(connection.providerDomain, delegation.providerDomain) ||
279
- !sameProviderDomain(connection.providerDomain, "api.atlassian.com")
280
- ) {
281
- continue;
282
- }
283
- personal.push({ connection, subjectId: delegation.ownerSubjectId });
284
- }
285
- return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
294
+ return [];
286
295
  }
287
296
 
288
297
  export function selectedPersonalConnectionServers(
@@ -301,31 +310,30 @@ function isNativeSubjectConnectionRef(
301
310
  return ref?.subjectScope === "subject" && ref.authoritySource !== "host";
302
311
  }
303
312
 
304
- function canonicalPersonalConnections(connections: ConnectionMetadata[]): ConnectionMetadata[] {
305
- return [...connections].sort((left, right) => {
306
- const active = Number(right.status === "active") - Number(left.status === "active");
307
- if (active !== 0) return active;
308
- const updated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
309
- if (updated !== 0) return updated;
310
- const created = Date.parse(right.createdAt) - Date.parse(left.createdAt);
311
- if (created !== 0) return created;
312
- return right.id.localeCompare(left.id);
313
- });
314
- }
315
-
316
313
  function sameProviderDomain(left: string, right: string): boolean {
317
314
  return left.toLowerCase() === right.toLowerCase();
318
315
  }
319
316
 
317
+ function canonicalConnectionResource(value: string): string {
318
+ try {
319
+ const url = new URL(value);
320
+ if (url.protocol !== "http:" && url.protocol !== "https:") return value.trim();
321
+ url.hash = "";
322
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
323
+ return url.toString();
324
+ } catch {
325
+ return value.trim();
326
+ }
327
+ }
328
+
320
329
  export function personalConnectionDelegationsFromVisibleConnections(input: {
321
330
  servers: McpServerConfig[];
322
331
  subjectId: string;
323
332
  connections: ConnectionMetadata[];
324
- authoritySelections?: McpConnectionAuthoritySelection[];
325
- rejectUnselectedActivatedConnections?: boolean;
333
+ authoritySelections?: McpConnectionAccountSelection[];
326
334
  }): McpPersonalConnectionDelegation[] {
327
335
  const delegations: McpPersonalConnectionDelegation[] = [];
328
- const connections = canonicalPersonalConnections(input.connections);
336
+ const connections = input.connections;
329
337
  const selections = new Map(
330
338
  (input.authoritySelections ?? []).map((selection) => [selection.serverId, selection]),
331
339
  );
@@ -337,52 +345,51 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
337
345
  (candidate) =>
338
346
  candidate.subjectId === input.subjectId &&
339
347
  candidate.status === "active" &&
348
+ candidate.authorityId !== undefined &&
349
+ candidate.authorityId !== null &&
340
350
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
341
351
  (!ref.kind || candidate.kind === ref.kind) &&
352
+ (!ref.resource ||
353
+ (typeof candidate.metadata.resource === "string" &&
354
+ canonicalConnectionResource(candidate.metadata.resource) ===
355
+ canonicalConnectionResource(ref.resource))) &&
342
356
  (!ref.connectionId || candidate.id === ref.connectionId),
343
357
  );
344
- // An explicit accepted-work selection is authoritative. Canonical newest
345
- // ordering is only the bounded legacy/omission fallback; it must never
346
- // replace a caller's exact opaque connection UUID when several accounts
347
- // share one server/provider tuple.
358
+ if (!selection && eligible.length > 1) {
359
+ throw new ConnectionAccountSelectionError(
360
+ `Choose an account for ${server.id}: multiple connected accounts match.`,
361
+ {
362
+ version: 1,
363
+ reason: "ambiguous_account",
364
+ accounts: [{ serverId: server.id, connectionId: null, reason: "selection_unavailable" }],
365
+ },
366
+ );
367
+ }
348
368
  const connection = selection
349
369
  ? eligible.find((candidate) => candidate.id === selection.connectionId)
350
370
  : eligible[0];
351
371
  if (!connection) continue;
352
- if (connection.authorityId) {
353
- if (
354
- !selection ||
355
- selection.connectionId !== connection.id ||
356
- selection.userDelegation.authorityId !== connection.authorityId
357
- ) {
358
- if (input.rejectUnselectedActivatedConnections) {
359
- throw new Error(
360
- `scheduled connection authority selection is required for activated server ${server.id}`,
361
- );
362
- }
363
- // Activated organization-user connections are never admitted through
364
- // the legacy owner tuple. The caller must select one exact opaque grant.
365
- continue;
366
- }
367
- selections.delete(server.id);
368
- } else if (selection) {
369
- throw new Error(`connection authority is not available for legacy server ${server.id}`);
370
- }
372
+ selections.delete(server.id);
371
373
  delegations.push({
372
374
  serverId: server.id,
373
375
  connectionId: connection.id,
374
- ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
376
+ originWorkspaceId: connection.workspaceId,
375
377
  ownerSubjectId: input.subjectId,
376
378
  providerDomain: connection.providerDomain,
377
379
  kind: connection.kind,
378
- ...(selection ? { userDelegation: selection.userDelegation } : {}),
379
380
  });
380
381
  }
381
382
  if (selections.size > 0) {
382
- throw new Error(
383
- `connection authority selection did not match an active selected server: ${[
384
- ...selections.keys(),
385
- ].join(", ")}`,
383
+ throw new ConnectionAccountSelectionError(
384
+ `Selected account is unavailable for: ${[...selections.keys()].join(", ")}`,
385
+ {
386
+ version: 1,
387
+ reason: "selected_account_unavailable",
388
+ accounts: [...selections.values()].map((selection) => ({
389
+ ...selection,
390
+ reason: "selection_unavailable",
391
+ })),
392
+ },
386
393
  );
387
394
  }
388
395
  return delegations;
@@ -393,39 +400,27 @@ export function personalConnectionDelegationsFromParent(input: {
393
400
  parentDelegations: McpPersonalConnectionDelegation[];
394
401
  personalGitHubResources?: ResourceRef[];
395
402
  targetSessionId?: string;
396
- rejectActivatedConnections?: boolean;
397
403
  }): McpPersonalConnectionDelegation[] {
398
- // Every successor is new logical work. `once` remains bound to the original
399
- // accepted turn, `session` may be re-admitted only into that exact session,
400
- // and `always` may cross to another authorized session.
401
- const childEligible = (delegation: McpPersonalConnectionDelegation) => {
402
- const authority = delegation.userDelegation;
403
- if (!authority) return true;
404
- if (authority.mode === "once") return false;
405
- if (authority.mode === "session") return authority.sessionId === input.targetSessionId;
406
- return true;
407
- };
408
404
  const mcp = input.servers.flatMap((server) => {
409
405
  const ref = server.connectionRef;
410
406
  if (!isNativeSubjectConnectionRef(ref)) return [];
411
407
  const delegation = input.parentDelegations.find(
412
408
  (candidate) =>
409
+ candidate.connectionType !== "atlassian" &&
413
410
  candidate.serverId === server.id &&
414
411
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
415
412
  (!ref.kind || !candidate.kind || candidate.kind === ref.kind),
416
413
  );
417
- return delegation && childEligible(delegation) ? [{ ...delegation }] : [];
414
+ return delegation ? [{ ...delegation }] : [];
418
415
  });
419
416
  const projected = [
420
417
  ...mcp,
421
418
  ...input.parentDelegations
422
419
  .filter(
423
420
  (item) =>
424
- childEligible(item) &&
425
- (item.connectionType === "social" ||
426
- item.connectionType === "atlassian" ||
427
- item.connectionType === "github_personal" ||
428
- item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID),
421
+ item.connectionType === "social" ||
422
+ item.connectionType === "github_personal" ||
423
+ item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
429
424
  )
430
425
  .map((item) => ({ ...item })),
431
426
  ];
@@ -464,14 +459,6 @@ export function personalConnectionDelegationsFromParent(input: {
464
459
  ) {
465
460
  throw new Error("agent-created personal GitHub repository authority is unavailable");
466
461
  }
467
- if (
468
- input.rejectActivatedConnections &&
469
- inherited.some((delegation) => delegation.userDelegation)
470
- ) {
471
- throw new Error(
472
- "scheduled connection authority is not available until task occurrence authority is activated",
473
- );
474
- }
475
462
  return inherited;
476
463
  }
477
464
 
@@ -484,14 +471,14 @@ export function personalConnectionDelegationsFromParent(input: {
484
471
  export function googleDrivePublicationDelegationFromVisibleConnections(input: {
485
472
  subjectId: string;
486
473
  connections: ConnectionMetadata[];
487
- authoritySelection?: McpConnectionAuthoritySelection;
488
- rejectUnselectedActivatedConnection?: boolean;
474
+ authoritySelection?: McpConnectionAccountSelection;
489
475
  }): McpPersonalConnectionDelegation | null {
490
476
  const selection = input.authoritySelection;
491
477
  const eligible = input.connections.filter((connection) => {
492
478
  if (
493
479
  connection.subjectId !== input.subjectId ||
494
480
  connection.status !== "active" ||
481
+ !connection.authorityId ||
495
482
  connection.kind !== "oauth2" ||
496
483
  !sameProviderDomain(connection.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) ||
497
484
  !googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
@@ -510,26 +497,25 @@ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
510
497
  ? eligible.filter((connection) => connection.id === selection.connectionId)
511
498
  : eligible;
512
499
  if (selection && selectedEligible.length !== 1) {
513
- throw new Error("Google Drive publication authority selection is unavailable");
500
+ throw new ConnectionAccountSelectionError(
501
+ "Google Drive publication authority selection is unavailable",
502
+ {
503
+ version: 1,
504
+ reason: "selected_account_unavailable",
505
+ accounts: [
506
+ {
507
+ serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
508
+ connectionId: selection.connectionId,
509
+ reason: "selection_unavailable",
510
+ },
511
+ ],
512
+ },
513
+ );
514
514
  }
515
515
  if (!selection && selectedEligible.length !== 1) return null;
516
516
  const connection = selectedEligible[0]!;
517
- if (connection.authorityId) {
518
- if (
519
- !selection ||
520
- selection.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID ||
521
- selection.connectionId !== connection.id ||
522
- selection.userDelegation.authorityId !== connection.authorityId
523
- ) {
524
- if (input.rejectUnselectedActivatedConnection) {
525
- throw new Error(
526
- "scheduled connection authority selection is required for activated Google Drive publication",
527
- );
528
- }
529
- return null;
530
- }
531
- } else if (selection) {
532
- throw new Error("connection authority is not available for legacy Google Drive publication");
517
+ if (selection && selection.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
518
+ throw new Error("Google Drive publication account selection is invalid");
533
519
  }
534
520
  // Freeze the exact output destination on the accepted delegation: a later
535
521
  // connection-settings change must never redirect an already-accepted turn's
@@ -542,44 +528,20 @@ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
542
528
  return {
543
529
  serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
544
530
  connectionId: connection.id,
545
- ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
531
+ originWorkspaceId: connection.workspaceId,
546
532
  ownerSubjectId: input.subjectId,
547
533
  providerDomain: connection.providerDomain,
548
534
  kind: connection.kind,
549
- ...(selection ? { userDelegation: selection.userDelegation } : {}),
550
535
  outputDestination: frozenMetadata.outputDestination,
551
536
  };
552
537
  }
553
538
 
554
- export function personalAtlassianDelegationsFromVisibleConnections(input: {
539
+ /** Native Atlassian credentials remain stored but are not new turn authority. */
540
+ export function personalAtlassianDelegationsFromVisibleConnections(_input: {
555
541
  subjectId: string;
556
542
  connections: ConnectionMetadata[];
557
- rejectActivatedConnections?: boolean;
558
543
  }): McpPersonalConnectionDelegation[] {
559
- const eligible = input.connections.filter(
560
- (connection) =>
561
- connection.subjectId === input.subjectId &&
562
- connection.status === "active" &&
563
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
564
- );
565
- if (input.rejectActivatedConnections && eligible.some((connection) => connection.authorityId)) {
566
- throw new Error(
567
- "scheduled connection authority selection is required for activated Atlassian access",
568
- );
569
- }
570
- // Atlassian's first-party multi-call adapter is a named successor. Until it
571
- // can consume an accepted snapshot, common-authority rows are omitted rather
572
- // than falling through the legacy owner-tuple resolver.
573
- return eligible
574
- .filter((connection) => !connection.authorityId)
575
- .map((connection) => ({
576
- serverId: `atlassian:${connection.id}`,
577
- connectionId: connection.id,
578
- ownerSubjectId: input.subjectId,
579
- providerDomain: connection.providerDomain,
580
- kind: connection.kind,
581
- connectionType: "atlassian" as const,
582
- }));
544
+ return [];
583
545
  }
584
546
 
585
547
  async function personalGitHubDelegationFromVisibleConnections(input: {
@@ -588,7 +550,7 @@ async function personalGitHubDelegationFromVisibleConnections(input: {
588
550
  subjectId: string;
589
551
  connections: ConnectionMetadata[];
590
552
  resources: ResourceRef[];
591
- authoritySelection?: McpConnectionAuthoritySelection;
553
+ authoritySelection?: McpConnectionAccountSelection;
592
554
  }): Promise<McpPersonalConnectionDelegation | null> {
593
555
  const resources = personalGitHubRepositoryResources(input.resources);
594
556
  if (resources.length === 0) {
@@ -602,27 +564,41 @@ async function personalGitHubDelegationFromVisibleConnections(input: {
602
564
  throw new Error("accepted work may use only one personal GitHub account");
603
565
  }
604
566
  const authoritySelection = input.authoritySelection;
605
- if (!authoritySelection) {
606
- throw new Error("personal GitHub repository resources require explicit connection authority");
607
- }
567
+ const credentialBindingId = [...bindingIds][0]!;
608
568
  const connection = input.connections.find(
609
569
  (candidate) =>
610
- candidate.id === authoritySelection.connectionId &&
570
+ (!authoritySelection || candidate.id === authoritySelection.connectionId) &&
611
571
  candidate.subjectId === input.subjectId &&
612
572
  candidate.status === "active" &&
613
- isPersonalGitHubConnection(candidate),
573
+ isPersonalGitHubConnection(candidate) &&
574
+ PersonalGitHubConnectionMetadata.safeParse(candidate.metadata).data?.credentialBindingId ===
575
+ credentialBindingId,
614
576
  );
615
577
  if (
616
578
  !connection ||
617
579
  !connection.authorityId ||
618
- connection.authorityId !== authoritySelection.userDelegation.authorityId ||
619
580
  connection.grantedScopes.length !== 1 ||
620
581
  connection.grantedScopes[0] !== "repo"
621
582
  ) {
583
+ if (authoritySelection) {
584
+ throw new ConnectionAccountSelectionError(
585
+ "personal GitHub connection authority selection is unavailable",
586
+ {
587
+ version: 1,
588
+ reason: "selected_account_unavailable",
589
+ accounts: [
590
+ {
591
+ serverId: PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
592
+ connectionId: authoritySelection.connectionId,
593
+ reason: "selection_unavailable",
594
+ },
595
+ ],
596
+ },
597
+ );
598
+ }
622
599
  throw new Error("personal GitHub connection authority selection is unavailable");
623
600
  }
624
601
  const metadata = PersonalGitHubConnectionMetadata.parse(connection.metadata);
625
- const credentialBindingId = [...bindingIds][0]!;
626
602
  if (metadata.credentialBindingId !== credentialBindingId) {
627
603
  throw new Error("personal GitHub credential binding does not match the selected connection");
628
604
  }
@@ -669,7 +645,6 @@ async function personalGitHubDelegationFromVisibleConnections(input: {
669
645
  providerDomain: "github.com",
670
646
  kind: "oauth2",
671
647
  connectionType: "github_personal",
672
- userDelegation: authoritySelection.userDelegation,
673
648
  personalGitHubRepositorySelection: {
674
649
  credentialBindingId,
675
650
  connectionAuthorityGeneration: selection.connectionAuthorityGeneration,
@@ -689,13 +664,12 @@ export function personalConnectionDelegationsEqual(
689
664
  const other = byServer.get(delegation.serverId);
690
665
  return (
691
666
  other?.connectionId === delegation.connectionId &&
667
+ other.canonicalServerId === delegation.canonicalServerId &&
692
668
  other.originWorkspaceId === delegation.originWorkspaceId &&
693
669
  other.ownerSubjectId === delegation.ownerSubjectId &&
694
670
  sameProviderDomain(other.providerDomain, delegation.providerDomain) &&
695
671
  other.kind === delegation.kind &&
696
672
  other.connectionType === delegation.connectionType &&
697
- JSON.stringify(other.userDelegation ?? null) ===
698
- JSON.stringify(delegation.userDelegation ?? null) &&
699
673
  JSON.stringify(other.personalGitHubRepositorySelection ?? null) ===
700
674
  JSON.stringify(delegation.personalGitHubRepositorySelection ?? null)
701
675
  );
@@ -776,11 +750,7 @@ export function withFrozenPersonalConnectionDelegations(input: {
776
750
  : publicationDelegations.length === 1
777
751
  ? publicationDelegations[0]!
778
752
  : null;
779
- if (
780
- !delegation ||
781
- (!delegation.userDelegation &&
782
- !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId)))
783
- ) {
753
+ if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
784
754
  return personalAuthorityUnavailable(request);
785
755
  }
786
756
  effectiveRequest = {
@@ -802,6 +772,214 @@ export function withFrozenPersonalConnectionDelegations(input: {
802
772
  };
803
773
  }
804
774
 
775
+ /** Freeze all native account routes without conflating workspace ownership
776
+ * with the exact causal human required for personal connections. */
777
+ export async function freezeConnectionAccounts(
778
+ input: Parameters<typeof freezePersonalConnectionDelegations>[0] & { accountId: string },
779
+ ): Promise<{
780
+ mcpAccountBindings: McpConnectionAccountBinding[] | null;
781
+ personalConnectionDelegations: McpPersonalConnectionDelegation[];
782
+ /**
783
+ * What the request decided, to be stored on the accepted turn: whose
784
+ * personal accounts it may use and which accounts were explicitly picked.
785
+ * Agent-created work carries its parent turn's intent (null when the parent
786
+ * predates intent).
787
+ */
788
+ connectionAccountIntent: ConnectionAccountIntent | null;
789
+ }> {
790
+ const selectedIds = new Set(input.tools.map((tool) => tool.id));
791
+ const servers = input.settings.mcpServers.filter(
792
+ (server) =>
793
+ selectedIds.has(server.id) &&
794
+ server.connectionRef &&
795
+ server.connectionRef.authoritySource !== "host",
796
+ );
797
+ const serverIds = new Set(servers.map((server) => server.id));
798
+ let mcpAccountBindings: McpConnectionAccountBinding[];
799
+ let connectionAccountIntent: ConnectionAccountIntent | null;
800
+ if (input.source.kind === "turn") {
801
+ if (input.authoritySelections?.length) {
802
+ throw new ConnectionAccountSelectionError(
803
+ "Agent-created work inherits the exact parent accounts",
804
+ { version: 1, reason: "parent_accounts_required", accounts: [] },
805
+ );
806
+ }
807
+ connectionAccountIntent = await getSessionTurnConnectionAccountIntent(
808
+ input.db,
809
+ input.workspaceId,
810
+ input.source.sessionId,
811
+ input.source.turnId,
812
+ );
813
+ const inherited = await getSessionTurnMcpAccountBindings(
814
+ input.db,
815
+ input.workspaceId,
816
+ input.source.sessionId,
817
+ input.source.turnId,
818
+ );
819
+ if (inherited === null) {
820
+ return {
821
+ mcpAccountBindings: null,
822
+ personalConnectionDelegations: await freezePersonalConnectionDelegations(input),
823
+ connectionAccountIntent,
824
+ };
825
+ }
826
+ mcpAccountBindings = inherited.filter((binding) => serverIds.has(binding.canonicalServerId));
827
+ } else {
828
+ connectionAccountIntent = {
829
+ version: 1,
830
+ principal:
831
+ input.source.kind === "subject"
832
+ ? { kind: "subject", subjectId: input.source.subjectId }
833
+ : { kind: "none" },
834
+ selections: [...(input.authoritySelections ?? [])],
835
+ };
836
+ mcpAccountBindings = mcpAccountBindingsFromVisibleConnections({
837
+ accountId: input.accountId,
838
+ workspaceId: input.workspaceId,
839
+ subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
840
+ servers,
841
+ connections: await visibleMcpAccountConnections(input.db, {
842
+ accountId: input.accountId,
843
+ workspaceId: input.workspaceId,
844
+ source: input.source,
845
+ }),
846
+ ...(input.authoritySelections
847
+ ? {
848
+ selections: input.authoritySelections.filter((selection) =>
849
+ serverIds.has(selection.serverId),
850
+ ),
851
+ }
852
+ : {}),
853
+ });
854
+ }
855
+ // Dedicated first-party publication/repository/social surfaces keep their
856
+ // existing selection and authority contracts; they are not generic MCP routes.
857
+ const special = await freezePersonalConnectionDelegations({
858
+ ...input,
859
+ settings: {
860
+ ...input.settings,
861
+ mcpServers: input.settings.mcpServers.filter((server) => !serverIds.has(server.id)),
862
+ },
863
+ ...(input.authoritySelections
864
+ ? {
865
+ authoritySelections: input.authoritySelections.filter(
866
+ (selection) => !serverIds.has(selection.serverId),
867
+ ),
868
+ }
869
+ : {}),
870
+ });
871
+ return {
872
+ mcpAccountBindings,
873
+ personalConnectionDelegations: [
874
+ ...personalDelegationsForAccountBindings(mcpAccountBindings),
875
+ ...special,
876
+ ],
877
+ connectionAccountIntent,
878
+ };
879
+ }
880
+
881
+ /** Shared workspace accounts plus the entitled owner's own active accounts. */
882
+ export async function visibleMcpAccountConnections(
883
+ db: Database,
884
+ input: {
885
+ accountId: string;
886
+ workspaceId: string;
887
+ source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
888
+ },
889
+ ): Promise<ConnectionMetadata[]> {
890
+ const personal =
891
+ input.source.kind === "subject" &&
892
+ (await ownerStillBelongsToWorkspace(db, {
893
+ accountId: input.accountId,
894
+ workspaceId: input.workspaceId,
895
+ subjectId: input.source.subjectId,
896
+ }))
897
+ ? await listOwnConnectionMetadata(db, {
898
+ accountId: input.accountId,
899
+ workspaceId: input.workspaceId,
900
+ subjectId: input.source.subjectId,
901
+ })
902
+ : [];
903
+ const workspace = await listConnectionsMetadata(db, input.workspaceId, null);
904
+ return [...workspace.filter((connection) => connection.subjectId === null), ...personal];
905
+ }
906
+
907
+ /**
908
+ * `visibleMcpAccountConnections` restricted to exact connection ids, for a live
909
+ * target fence. The same visibility rules apply (shared workspace accounts;
910
+ * the entitled owner's own active accounts while the owner still belongs to
911
+ * the workspace), but unrelated accounts are never read. An id read on the
912
+ * wrong side, or no longer visible, is simply absent.
913
+ */
914
+ export async function visibleMcpAccountConnectionsByIds(
915
+ db: Database,
916
+ input: {
917
+ accountId: string;
918
+ workspaceId: string;
919
+ source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
920
+ workspaceConnectionIds: readonly string[];
921
+ personalConnectionIds: readonly string[];
922
+ },
923
+ ): Promise<ConnectionMetadata[]> {
924
+ const workspace: ConnectionMetadata[] = [];
925
+ // `db` may be one bounded transaction: these reads must not overlap.
926
+ for (const connectionId of new Set(input.workspaceConnectionIds)) {
927
+ const connection = await getConnectionMetadata(db, input.workspaceId, connectionId, null);
928
+ if (connection && connection.subjectId === null) workspace.push(connection);
929
+ }
930
+ const personalIds = new Set(input.personalConnectionIds);
931
+ const personal =
932
+ input.source.kind === "subject" &&
933
+ personalIds.size > 0 &&
934
+ (await ownerStillBelongsToWorkspace(db, {
935
+ accountId: input.accountId,
936
+ workspaceId: input.workspaceId,
937
+ subjectId: input.source.subjectId,
938
+ }))
939
+ ? await listOwnConnectionMetadata(db, {
940
+ accountId: input.accountId,
941
+ workspaceId: input.workspaceId,
942
+ subjectId: input.source.subjectId,
943
+ connectionIds: personalIds,
944
+ })
945
+ : [];
946
+ return [...workspace, ...personal];
947
+ }
948
+
949
+ /**
950
+ * Every account a fresh, selection-free freeze would bind for the generic MCP
951
+ * routes among `tools`: exactly what `freezeConnectionAccounts` binds when no
952
+ * account is chosen. The same entitlement contract applies: `source` is a
953
+ * grant-derived or frozen-owner subject, never a looked-up one. Read-only; it
954
+ * never persists a delegation.
955
+ */
956
+ export async function availableMcpAccountBindings(input: {
957
+ db: Database;
958
+ accountId: string;
959
+ workspaceId: string;
960
+ settings: Pick<Settings, "mcpServers">;
961
+ tools: ToolRef[];
962
+ source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
963
+ /** Result of `visibleMcpAccountConnections` for the same source, when already read. */
964
+ connections?: ConnectionMetadata[];
965
+ }): Promise<McpConnectionAccountBinding[]> {
966
+ const selectedIds = new Set(input.tools.map((tool) => tool.id));
967
+ const servers = input.settings.mcpServers.filter(
968
+ (server) =>
969
+ selectedIds.has(server.id) &&
970
+ server.connectionRef &&
971
+ server.connectionRef.authoritySource !== "host",
972
+ );
973
+ if (servers.length === 0) return [];
974
+ return mcpAccountBindingsFromVisibleConnections({
975
+ accountId: input.accountId,
976
+ workspaceId: input.workspaceId,
977
+ subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
978
+ servers,
979
+ connections: input.connections ?? (await visibleMcpAccountConnections(input.db, input)),
980
+ });
981
+ }
982
+
805
983
  export async function freezePersonalConnectionDelegations(input: {
806
984
  db: Database;
807
985
  workspaceId: string;
@@ -809,8 +987,7 @@ export async function freezePersonalConnectionDelegations(input: {
809
987
  tools: ToolRef[];
810
988
  resources?: ResourceRef[];
811
989
  source: PersonalConnectionDelegationSource;
812
- authoritySelections?: McpConnectionAuthoritySelection[];
813
- rejectUnselectedActivatedConnections?: boolean;
990
+ authoritySelections?: McpConnectionAccountSelection[];
814
991
  /** Exact first-party export tool + permission gate, not broad opengeni attachment. */
815
992
  googleDrivePublicationEnabled?: boolean;
816
993
  /** Exact first-party Atlassian tool + permission gate. */
@@ -853,16 +1030,13 @@ export async function freezePersonalConnectionDelegations(input: {
853
1030
  ),
854
1031
  personalGitHubResources,
855
1032
  ...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
856
- ...(input.rejectUnselectedActivatedConnections !== undefined
857
- ? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
858
- : {}),
859
1033
  });
860
1034
  return inherited.filter((item) => {
861
1035
  if (item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
862
1036
  return input.googleDrivePublicationEnabled === true;
863
1037
  }
864
1038
  if (item.connectionType === "atlassian") {
865
- return includeFirstPartyConnections && input.atlassianEnabled === true;
1039
+ return false;
866
1040
  }
867
1041
  if (item.connectionType === "social") {
868
1042
  return includeFirstPartyConnections;
@@ -876,54 +1050,23 @@ export async function freezePersonalConnectionDelegations(input: {
876
1050
  workspaceId: input.workspaceId,
877
1051
  subjectId: ownerSubjectId,
878
1052
  });
879
- const targetLocalConnections = await listConnectionsMetadata(
880
- input.db,
881
- input.workspaceId,
882
- ownerSubjectId,
883
- );
884
- const portableSelections = await Promise.all(
885
- (input.authoritySelections ?? []).map(async (selection) => {
886
- const originWorkspaceId = await resolvePersonalConnectionAuthoritySelectionOrigin(input.db, {
887
- accountId: selection.userDelegation.organizationId,
888
- targetWorkspaceId: input.workspaceId,
889
- subjectId: ownerSubjectId,
890
- connectionId: selection.connectionId,
891
- delegation: selection.userDelegation,
892
- });
893
- if (!originWorkspaceId) {
894
- throw new Error(`connection authority selection is unavailable: ${selection.serverId}`);
895
- }
896
- const connection = await getConnectionMetadata(
897
- input.db,
898
- originWorkspaceId,
899
- selection.connectionId,
900
- ownerSubjectId,
1053
+ if (!membership) {
1054
+ if ((input.authoritySelections?.length ?? 0) > 0) {
1055
+ throw new ConnectionAccountSelectionError(
1056
+ "Selected connection account requires live owner workspace access",
1057
+ { version: 1, reason: "owner_access_unavailable", accounts: [] },
901
1058
  );
902
- if (
903
- !connection ||
904
- connection.workspaceId !== originWorkspaceId ||
905
- connection.subjectId !== ownerSubjectId ||
906
- connection.authorityId !== selection.userDelegation.authorityId
907
- ) {
908
- throw new Error(`connection authority selection is unavailable: ${selection.serverId}`);
909
- }
910
- return connection;
911
- }),
912
- );
913
- if (!membership && portableSelections.length === 0) {
1059
+ }
914
1060
  if (personalGitHubResources.length > 0) {
915
1061
  throw new Error("personal GitHub repository authority requires live causal user authority");
916
1062
  }
917
1063
  return [];
918
1064
  }
919
- const visibleConnections = [
920
- ...new Map(
921
- [...targetLocalConnections, ...portableSelections].map((connection) => [
922
- connection.id,
923
- connection,
924
- ]),
925
- ).values(),
926
- ];
1065
+ const visibleConnections = await listOwnConnectionMetadata(input.db, {
1066
+ accountId: input.source.accountId,
1067
+ workspaceId: input.workspaceId,
1068
+ subjectId: ownerSubjectId,
1069
+ });
927
1070
  const selectedMcpServerIds = new Set(servers.map((server) => server.id));
928
1071
  const supportedSelectionIds = new Set(selectedMcpServerIds);
929
1072
  if (input.googleDrivePublicationEnabled) {
@@ -936,10 +1079,18 @@ export async function freezePersonalConnectionDelegations(input: {
936
1079
  (selection) => !supportedSelectionIds.has(selection.serverId),
937
1080
  );
938
1081
  if (unsupportedSelections.length > 0) {
939
- throw new Error(
1082
+ throw new ConnectionAccountSelectionError(
940
1083
  `connection authority selection did not match a selected MCP server: ${unsupportedSelections
941
1084
  .map((selection) => selection.serverId)
942
1085
  .join(", ")}`,
1086
+ {
1087
+ version: 1,
1088
+ reason: "selected_account_unavailable",
1089
+ accounts: unsupportedSelections.map((selection) => ({
1090
+ ...selection,
1091
+ reason: "connector_unavailable",
1092
+ })),
1093
+ },
943
1094
  );
944
1095
  }
945
1096
  const mcp = personalConnectionDelegationsFromVisibleConnections({
@@ -949,9 +1100,6 @@ export async function freezePersonalConnectionDelegations(input: {
949
1100
  authoritySelections: (input.authoritySelections ?? []).filter((selection) =>
950
1101
  selectedMcpServerIds.has(selection.serverId),
951
1102
  ),
952
- ...(input.rejectUnselectedActivatedConnections !== undefined
953
- ? { rejectUnselectedActivatedConnections: input.rejectUnselectedActivatedConnections }
954
- : {}),
955
1103
  });
956
1104
  const personalGitHubAuthoritySelection = (input.authoritySelections ?? []).find(
957
1105
  (selection) => selection.serverId === PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
@@ -986,9 +1134,6 @@ export async function freezePersonalConnectionDelegations(input: {
986
1134
  ? personalAtlassianDelegationsFromVisibleConnections({
987
1135
  subjectId: ownerSubjectId,
988
1136
  connections: visibleConnections,
989
- ...(input.rejectUnselectedActivatedConnections !== undefined
990
- ? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
991
- : {}),
992
1137
  })
993
1138
  : [];
994
1139
  const googleDriveAuthoritySelection = input.googleDrivePublicationEnabled
@@ -1003,9 +1148,6 @@ export async function freezePersonalConnectionDelegations(input: {
1003
1148
  ...(googleDriveAuthoritySelection
1004
1149
  ? { authoritySelection: googleDriveAuthoritySelection }
1005
1150
  : {}),
1006
- ...(input.rejectUnselectedActivatedConnections !== undefined
1007
- ? { rejectUnselectedActivatedConnection: input.rejectUnselectedActivatedConnections }
1008
- : {}),
1009
1151
  })
1010
1152
  : null;
1011
1153
  return [