@opengeni/core 0.28.1 → 1.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18812 -6833
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -2,27 +2,101 @@ import type { McpServerConfig, Settings } from "@opengeni/config";
2
2
  import type {
3
3
  AccessGrant,
4
4
  ConnectionMetadata,
5
+ McpConnectionAccountBinding,
6
+ McpConnectionAccountSelection,
5
7
  McpPersonalConnectionDelegation,
8
+ McpServerConnectionRef,
9
+ ResourceRef,
6
10
  SessionTurn,
7
11
  SocialConnection,
8
12
  ToolRef,
9
13
  } from "@opengeni/contracts";
10
14
  import {
15
+ GOOGLE_DRIVE_PROVIDER_DOMAIN,
16
+ GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
17
+ GoogleDriveConnectionMetadata,
18
+ googleDriveScopesAllowCapability,
19
+ } from "@opengeni/contracts/google-drive";
20
+ import {
21
+ isPersonalGitHubConnection,
22
+ PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
23
+ PersonalGitHubConnectionMetadata,
24
+ } from "@opengeni/contracts/personal-github";
25
+ import {
26
+ getPersonalGitHubRepositorySelectionState,
11
27
  getSessionTurnPersonalConnectionDelegations,
28
+ getSessionTurnMcpAccountBindings,
12
29
  getConnectionMetadata,
13
30
  getSocialConnection,
14
- getWorkspaceGrant,
15
31
  listConnectionsMetadata,
16
32
  listSocialConnections,
33
+ listOwnedConnectionAccounts,
34
+ namedSubjectHasLiveWorkspaceAuthority,
17
35
  type Database,
18
36
  type ResolveConnectionCredentialInput,
19
37
  type ResolveConnectionCredentialResult,
20
38
  } from "@opengeni/db";
39
+ import { personalGitHubRepositoryResources } from "./resources";
40
+ import { ConnectionAccountSelectionError } from "./connection-account-selection-error";
41
+ export { ConnectionAccountSelectionError } from "./connection-account-selection-error";
42
+ import {
43
+ mcpAccountBindingsFromVisibleConnections,
44
+ personalDelegationsForAccountBindings,
45
+ } from "./mcp-account-bindings";
21
46
 
22
47
  export type PersonalConnectionDelegationSource =
23
- | { kind: "subject"; subjectId: string }
48
+ | {
49
+ kind: "subject";
50
+ subjectId: string;
51
+ /**
52
+ * Organization of the authorizing grant. The owner-only
53
+ * personal-workspace pointer lives on an organization membership, so
54
+ * the account is part of the question, not an optimization.
55
+ */
56
+ accountId: string;
57
+ }
24
58
  | { kind: "turn"; sessionId: string; turnId: string }
25
59
  | { kind: "none" };
60
+
61
+ /**
62
+ * Is `subjectId` still authorized in `workspaceId`?
63
+ *
64
+ * Personal-connection authority has always been "the owner must still belong
65
+ * to this workspace", but the old check was `getWorkspaceGrant` - a bare
66
+ * `workspace_memberships` join. A managed human's personal workspace
67
+ * deliberately has no row in that table (migration 0219 raises on one), so the
68
+ * join answered `false` for the one person who always belongs, and every
69
+ * personal connection silently disappeared inside the owner's own private
70
+ * workspace.
71
+ *
72
+ * `namedSubjectHasLiveWorkspaceAuthority` models both halves - the membership
73
+ * row and the organization membership's `personal_workspace_id` pointer - but
74
+ * it is an **oracle, not an authorization**: it answers for whatever subject it
75
+ * is handed and cannot establish who the caller is (see its doc comment in
76
+ * `@opengeni/db`). Every use is safe only because *this* module has already
77
+ * established entitlement to ask, locally:
78
+ *
79
+ * - at freeze time the subject is `source.subjectId`, and
80
+ * `personalConnectionDelegationSourceForGrant` admits only a grant whose
81
+ * `subjectId` came from a real authenticated principal. Agent-attempt,
82
+ * service, service-initiated, and **delegated/bearer** grants never reach
83
+ * this branch: they resolve to `{kind:"none"}`, or - for a worker-signed
84
+ * agent attempt - to `{kind:"turn"}`, which probes only a frozen
85
+ * `ownerSubjectId` already persisted for the grant's own workspace. Either
86
+ * way an unvalidated signed-token subject is never the probed subject; and
87
+ * - on the `*ForGrant` paths the subject is the frozen `ownerSubjectId` of a
88
+ * delegation already persisted for the workspace the caller is already
89
+ * authorized in.
90
+ *
91
+ * Do not pass any other subject through this helper. A subject that was merely
92
+ * looked up, guessed, or supplied by a caller is not entitled.
93
+ */
94
+ async function ownerStillBelongsToWorkspace(
95
+ db: Database,
96
+ input: { accountId: string; workspaceId: string; subjectId: string },
97
+ ): Promise<boolean> {
98
+ return await namedSubjectHasLiveWorkspaceAuthority(db, input);
99
+ }
26
100
  export type AuthorizedSocialConnection = { connection: SocialConnection; subjectId: string | null };
27
101
  export type AuthorizedAtlassianConnection = {
28
102
  connection: ConnectionMetadata;
@@ -47,17 +121,100 @@ export function personalConnectionDelegationSourceForGrant(
47
121
  ): PersonalConnectionDelegationSource {
48
122
  const callerSessionId = grant.metadata?.["sessionId"];
49
123
  const callerTurnId = grant.metadata?.["turnId"];
124
+ // LOAD-BEARING ORDERING. This turn branch sits ABOVE the delegated filter
125
+ // below, so a grant carrying both `sessionId` and `turnId` never reaches that
126
+ // filter. That is safe only because `DelegatedAccessTokenPayload`'s
127
+ // `superRefine` forbids `principalKind: "human_session"` from carrying any
128
+ // attempt claim (`turnId`/`attemptId`/`executionGeneration`), so a delegated
129
+ // human-session bearer is structurally unable to take this branch. If that
130
+ // refinement is ever relaxed, a delegated bearer could route around the
131
+ // delegated filter by presenting a turn reference - move the filter above
132
+ // this branch at the same time. Pinned by
133
+ // `packages/contracts/test/delegated-access-token-attempt-claims.test.ts`.
50
134
  if (typeof callerSessionId === "string" && typeof callerTurnId === "string") {
51
135
  return { kind: "turn", sessionId: callerSessionId, turnId: callerTurnId };
52
136
  }
53
137
  if (
54
138
  grant.principalKind === "agent_attempt" ||
55
139
  grant.principalKind === "service" ||
56
- grant.serviceInitiator
140
+ grant.serviceInitiator ||
141
+ // A delegated bearer's grant is built entirely from signed token payload
142
+ // (`delegatedAccessContext`); no database row binds its `subjectId` or
143
+ // `workspaceId` to anything. It is therefore never an entitled subject for
144
+ // the owner-only personal-workspace pointer, which CLAUDE.md reserves for
145
+ // the canonical managed-cookie session: "Bearer/delegated principals, API
146
+ // keys, and account or organization administrators receive no
147
+ // personal-workspace access through that exception." Before the pointer
148
+ // existed this was moot - `getWorkspaceGrant` returned null for a personal
149
+ // workspace regardless - so the filter belongs with the pointer.
150
+ //
151
+ // Why testing THIS field is sound while inspecting the others is not: a
152
+ // host signs `subjectId`, `principalKind`, and `serviceInitiator` into the
153
+ // token itself, so those are host-chosen and a hostile host picks whatever
154
+ // it likes. `metadata.delegated` is different - it is stamped by
155
+ // `delegatedAccessContext` in `@opengeni/core` AFTER the token signature is
156
+ // verified, and is not carried in the payload at all, so a host cannot
157
+ // clear it. Do not "harden" this by switching to `principalKind`. The
158
+ // strictly better shape is a positive assertion of how the request
159
+ // authenticated (a canonical managed-cookie stamp) rather than any
160
+ // inspection of the grant; that convergence is tracked separately.
161
+ grant.metadata?.delegated === true
57
162
  ) {
58
163
  return { kind: "none" };
59
164
  }
60
- return { kind: "subject", subjectId: grant.subjectId };
165
+ return { kind: "subject", subjectId: grant.subjectId, accountId: grant.accountId };
166
+ }
167
+
168
+ /** Account-picker inventory: shared workspace accounts and this sender's own
169
+ * personal accounts. Visibility is never an execution grant or a credential
170
+ * response; every selected connection is validated again at admission/use. */
171
+ export async function listOwnConnectionAccountsForGrant(
172
+ db: Database,
173
+ grant: AccessGrant,
174
+ options: { includeInactive?: boolean } = {},
175
+ ): Promise<ConnectionMetadata[]> {
176
+ const source = personalConnectionDelegationSourceForGrant(grant);
177
+ const workspace = (await listConnectionsMetadata(db, grant.workspaceId, null)).filter(
178
+ (connection) =>
179
+ connection.subjectId === null &&
180
+ connection.workspaceId === grant.workspaceId &&
181
+ connection.accountId === grant.accountId &&
182
+ (options.includeInactive === true || connection.status === "active"),
183
+ );
184
+ if (source.kind !== "subject") return workspace;
185
+ const personal = await listOwnConnectionMetadata(db, {
186
+ ...source,
187
+ workspaceId: grant.workspaceId,
188
+ includeInactive: options.includeInactive === true,
189
+ });
190
+ return [...workspace, ...personal];
191
+ }
192
+
193
+ async function listOwnConnectionMetadata(
194
+ db: Database,
195
+ input: { accountId: string; workspaceId: string; subjectId: string; includeInactive?: boolean },
196
+ ): Promise<ConnectionMetadata[]> {
197
+ const accounts = await listOwnedConnectionAccounts(db, input);
198
+ const connections: ConnectionMetadata[] = [];
199
+ // The caller may supply a transaction handle (for example, credential
200
+ // rotation). Each metadata read opens a nested RLS savepoint on that same
201
+ // connection, so these reads must not overlap.
202
+ for (const account of accounts) {
203
+ const connection = await getConnectionMetadata(
204
+ db,
205
+ account.originWorkspaceId,
206
+ account.connectionId,
207
+ input.subjectId,
208
+ );
209
+ if (
210
+ connection?.accountId === input.accountId &&
211
+ connection.subjectId === input.subjectId &&
212
+ connection.authorityId != null &&
213
+ (input.includeInactive === true || connection.status === "active")
214
+ )
215
+ connections.push(connection);
216
+ }
217
+ return connections;
61
218
  }
62
219
 
63
220
  export async function authorizedSocialConnectionsForGrant(input: {
@@ -96,7 +253,13 @@ export async function authorizedSocialConnectionsForGrant(input: {
96
253
  ).filter((item) => item.connectionType === "social");
97
254
  const personal: AuthorizedSocialConnection[] = [];
98
255
  for (const delegation of delegations) {
99
- if (!(await getWorkspaceGrant(input.db, delegation.ownerSubjectId, input.grant.workspaceId)))
256
+ if (
257
+ !(await ownerStillBelongsToWorkspace(input.db, {
258
+ accountId: input.grant.accountId,
259
+ workspaceId: input.grant.workspaceId,
260
+ subjectId: delegation.ownerSubjectId,
261
+ }))
262
+ )
100
263
  continue;
101
264
  const connection = await getSocialConnection(
102
265
  input.db,
@@ -112,68 +275,12 @@ export async function authorizedSocialConnectionsForGrant(input: {
112
275
  return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
113
276
  }
114
277
 
115
- export async function authorizedAtlassianConnectionsForGrant(input: {
278
+ /** Historical native Atlassian grants no longer authorize API execution. */
279
+ export async function authorizedAtlassianConnectionsForGrant(_input: {
116
280
  db: Database;
117
281
  grant: AccessGrant;
118
282
  }): Promise<AuthorizedAtlassianConnection[]> {
119
- const workspace = (await listConnectionsMetadata(input.db, input.grant.workspaceId, null)).filter(
120
- (connection) =>
121
- connection.subjectId === null &&
122
- connection.status === "active" &&
123
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
124
- );
125
- const source = personalConnectionDelegationSourceForGrant(input.grant);
126
- if (source.kind === "none") {
127
- return workspace.map((connection) => ({ connection, subjectId: null }));
128
- }
129
- if (source.kind === "subject") {
130
- const visible = await listConnectionsMetadata(
131
- input.db,
132
- input.grant.workspaceId,
133
- source.subjectId,
134
- );
135
- return visible
136
- .filter(
137
- (connection) =>
138
- connection.status === "active" &&
139
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
140
- )
141
- .map((connection) => ({
142
- connection,
143
- subjectId: connection.subjectId === null ? null : source.subjectId,
144
- }));
145
- }
146
- const delegations = (
147
- await getSessionTurnPersonalConnectionDelegations(
148
- input.db,
149
- input.grant.workspaceId,
150
- source.sessionId,
151
- source.turnId,
152
- )
153
- ).filter((item) => item.connectionType === "atlassian");
154
- const personal: AuthorizedAtlassianConnection[] = [];
155
- for (const delegation of delegations) {
156
- if (!(await getWorkspaceGrant(input.db, delegation.ownerSubjectId, input.grant.workspaceId))) {
157
- continue;
158
- }
159
- const connection = await getConnectionMetadata(
160
- input.db,
161
- input.grant.workspaceId,
162
- delegation.connectionId,
163
- delegation.ownerSubjectId,
164
- );
165
- if (
166
- !connection ||
167
- connection.subjectId !== delegation.ownerSubjectId ||
168
- connection.status !== "active" ||
169
- !sameProviderDomain(connection.providerDomain, delegation.providerDomain) ||
170
- !sameProviderDomain(connection.providerDomain, "api.atlassian.com")
171
- ) {
172
- continue;
173
- }
174
- personal.push({ connection, subjectId: delegation.ownerSubjectId });
175
- }
176
- return [...workspace.map((connection) => ({ connection, subjectId: null })), ...personal];
283
+ return [];
177
284
  }
178
285
 
179
286
  export function selectedPersonalConnectionServers(
@@ -182,77 +289,329 @@ export function selectedPersonalConnectionServers(
182
289
  ): McpServerConfig[] {
183
290
  const selected = new Set(tools.map((tool) => tool.id));
184
291
  return settings.mcpServers.filter(
185
- (server) => selected.has(server.id) && server.connectionRef?.subjectScope === "subject",
292
+ (server) => selected.has(server.id) && isNativeSubjectConnectionRef(server.connectionRef),
186
293
  );
187
294
  }
188
295
 
189
- function canonicalPersonalConnections(connections: ConnectionMetadata[]): ConnectionMetadata[] {
190
- return [...connections].sort((left, right) => {
191
- const active = Number(right.status === "active") - Number(left.status === "active");
192
- if (active !== 0) return active;
193
- const updated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
194
- if (updated !== 0) return updated;
195
- const created = Date.parse(right.createdAt) - Date.parse(left.createdAt);
196
- if (created !== 0) return created;
197
- return right.id.localeCompare(left.id);
198
- });
296
+ function isNativeSubjectConnectionRef(
297
+ ref: McpServerConnectionRef | null | undefined,
298
+ ): ref is McpServerConnectionRef & { subjectScope: "subject" } {
299
+ return ref?.subjectScope === "subject" && ref.authoritySource !== "host";
199
300
  }
200
301
 
201
302
  function sameProviderDomain(left: string, right: string): boolean {
202
303
  return left.toLowerCase() === right.toLowerCase();
203
304
  }
204
305
 
306
+ function canonicalConnectionResource(value: string): string {
307
+ try {
308
+ const url = new URL(value);
309
+ if (url.protocol !== "http:" && url.protocol !== "https:") return value.trim();
310
+ url.hash = "";
311
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
312
+ return url.toString();
313
+ } catch {
314
+ return value.trim();
315
+ }
316
+ }
317
+
205
318
  export function personalConnectionDelegationsFromVisibleConnections(input: {
206
319
  servers: McpServerConfig[];
207
320
  subjectId: string;
208
321
  connections: ConnectionMetadata[];
322
+ authoritySelections?: McpConnectionAccountSelection[];
209
323
  }): McpPersonalConnectionDelegation[] {
210
324
  const delegations: McpPersonalConnectionDelegation[] = [];
211
- const connections = canonicalPersonalConnections(input.connections);
325
+ const connections = input.connections;
326
+ const selections = new Map(
327
+ (input.authoritySelections ?? []).map((selection) => [selection.serverId, selection]),
328
+ );
212
329
  for (const server of input.servers) {
213
330
  const ref = server.connectionRef;
214
- if (!ref || ref.subjectScope !== "subject") continue;
215
- const connection = connections.find(
331
+ if (!isNativeSubjectConnectionRef(ref)) continue;
332
+ const selection = selections.get(server.id);
333
+ const eligible = connections.filter(
216
334
  (candidate) =>
217
335
  candidate.subjectId === input.subjectId &&
218
336
  candidate.status === "active" &&
337
+ candidate.authorityId !== undefined &&
338
+ candidate.authorityId !== null &&
219
339
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
220
340
  (!ref.kind || candidate.kind === ref.kind) &&
341
+ (!ref.resource ||
342
+ (typeof candidate.metadata.resource === "string" &&
343
+ canonicalConnectionResource(candidate.metadata.resource) ===
344
+ canonicalConnectionResource(ref.resource))) &&
221
345
  (!ref.connectionId || candidate.id === ref.connectionId),
222
346
  );
347
+ if (!selection && eligible.length > 1) {
348
+ throw new ConnectionAccountSelectionError(
349
+ `Choose an account for ${server.id}: multiple connected accounts match.`,
350
+ {
351
+ version: 1,
352
+ reason: "ambiguous_account",
353
+ accounts: [{ serverId: server.id, connectionId: null, reason: "selection_unavailable" }],
354
+ },
355
+ );
356
+ }
357
+ const connection = selection
358
+ ? eligible.find((candidate) => candidate.id === selection.connectionId)
359
+ : eligible[0];
223
360
  if (!connection) continue;
361
+ selections.delete(server.id);
224
362
  delegations.push({
225
363
  serverId: server.id,
226
364
  connectionId: connection.id,
365
+ originWorkspaceId: connection.workspaceId,
227
366
  ownerSubjectId: input.subjectId,
228
367
  providerDomain: connection.providerDomain,
229
368
  kind: connection.kind,
230
369
  });
231
370
  }
371
+ if (selections.size > 0) {
372
+ throw new ConnectionAccountSelectionError(
373
+ `Selected account is unavailable for: ${[...selections.keys()].join(", ")}`,
374
+ {
375
+ version: 1,
376
+ reason: "selected_account_unavailable",
377
+ accounts: [...selections.values()].map((selection) => ({
378
+ ...selection,
379
+ reason: "selection_unavailable",
380
+ })),
381
+ },
382
+ );
383
+ }
232
384
  return delegations;
233
385
  }
234
386
 
235
387
  export function personalConnectionDelegationsFromParent(input: {
236
388
  servers: McpServerConfig[];
237
389
  parentDelegations: McpPersonalConnectionDelegation[];
390
+ personalGitHubResources?: ResourceRef[];
391
+ targetSessionId?: string;
238
392
  }): McpPersonalConnectionDelegation[] {
239
393
  const mcp = input.servers.flatMap((server) => {
240
394
  const ref = server.connectionRef;
241
- if (!ref || ref.subjectScope !== "subject") return [];
395
+ if (!isNativeSubjectConnectionRef(ref)) return [];
242
396
  const delegation = input.parentDelegations.find(
243
397
  (candidate) =>
398
+ candidate.connectionType !== "atlassian" &&
244
399
  candidate.serverId === server.id &&
245
400
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
246
401
  (!ref.kind || !candidate.kind || candidate.kind === ref.kind),
247
402
  );
248
403
  return delegation ? [{ ...delegation }] : [];
249
404
  });
250
- return [
405
+ const projected = [
251
406
  ...mcp,
252
407
  ...input.parentDelegations
253
- .filter((item) => item.connectionType === "social" || item.connectionType === "atlassian")
408
+ .filter(
409
+ (item) =>
410
+ item.connectionType === "social" ||
411
+ item.connectionType === "github_personal" ||
412
+ item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
413
+ )
254
414
  .map((item) => ({ ...item })),
255
415
  ];
416
+ const requestedGitHub = personalGitHubRepositoryResources(input.personalGitHubResources ?? []);
417
+ const inherited = projected.flatMap((delegation) => {
418
+ if (delegation.connectionType !== "github_personal") return [delegation];
419
+ if (requestedGitHub.length === 0) return [];
420
+ const snapshot = delegation.personalGitHubRepositorySelection;
421
+ if (!snapshot) return [];
422
+ const repositories = requestedGitHub.map((resource) => {
423
+ const parent = snapshot.repositories.find(
424
+ (candidate) =>
425
+ candidate.repositoryId === resource.repositoryId &&
426
+ candidate.canonicalUrl === resource.uri &&
427
+ candidate.ref === resource.ref,
428
+ );
429
+ if (
430
+ !parent ||
431
+ resource.credentialBindingId !== snapshot.credentialBindingId ||
432
+ (resource.access === "write" && parent.access !== "write")
433
+ ) {
434
+ throw new Error("agent-created personal GitHub repository exceeds parent authority");
435
+ }
436
+ return { ...parent, access: resource.access };
437
+ });
438
+ return [
439
+ {
440
+ ...delegation,
441
+ personalGitHubRepositorySelection: { ...snapshot, repositories },
442
+ },
443
+ ];
444
+ });
445
+ if (
446
+ requestedGitHub.length > 0 &&
447
+ !inherited.some((delegation) => delegation.connectionType === "github_personal")
448
+ ) {
449
+ throw new Error("agent-created personal GitHub repository authority is unavailable");
450
+ }
451
+ return inherited;
452
+ }
453
+
454
+ /**
455
+ * Freezes Google Drive publishing only when one exact subject-owned connection
456
+ * is eligible. Multiple writable Google accounts are intentionally ambiguous:
457
+ * callers must narrow the connection before a later turn can advertise or use
458
+ * the private publication tool.
459
+ */
460
+ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
461
+ subjectId: string;
462
+ connections: ConnectionMetadata[];
463
+ authoritySelection?: McpConnectionAccountSelection;
464
+ }): McpPersonalConnectionDelegation | null {
465
+ const selection = input.authoritySelection;
466
+ const eligible = input.connections.filter((connection) => {
467
+ if (
468
+ connection.subjectId !== input.subjectId ||
469
+ connection.status !== "active" ||
470
+ !connection.authorityId ||
471
+ connection.kind !== "oauth2" ||
472
+ !sameProviderDomain(connection.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) ||
473
+ !googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
474
+ ) {
475
+ return false;
476
+ }
477
+ const metadata = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
478
+ return Boolean(
479
+ metadata.success &&
480
+ metadata.data.outputDestination &&
481
+ metadata.data.lifecycle?.state !== "paused" &&
482
+ (!metadata.data.lifecycle || metadata.data.lifecycle.state === "active"),
483
+ );
484
+ });
485
+ const selectedEligible = selection
486
+ ? eligible.filter((connection) => connection.id === selection.connectionId)
487
+ : eligible;
488
+ if (selection && selectedEligible.length !== 1) {
489
+ throw new Error("Google Drive publication authority selection is unavailable");
490
+ }
491
+ if (!selection && selectedEligible.length !== 1) return null;
492
+ const connection = selectedEligible[0]!;
493
+ if (selection && selection.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
494
+ throw new Error("Google Drive publication account selection is invalid");
495
+ }
496
+ // Freeze the exact output destination on the accepted delegation: a later
497
+ // connection-settings change must never redirect an already-accepted turn's
498
+ // publication. Eligibility above already proved the metadata parses and the
499
+ // destination exists.
500
+ const frozenMetadata = GoogleDriveConnectionMetadata.parse(connection.metadata);
501
+ if (!frozenMetadata.outputDestination) {
502
+ throw new Error("Google Drive publication destination disappeared during acceptance");
503
+ }
504
+ return {
505
+ serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
506
+ connectionId: connection.id,
507
+ originWorkspaceId: connection.workspaceId,
508
+ ownerSubjectId: input.subjectId,
509
+ providerDomain: connection.providerDomain,
510
+ kind: connection.kind,
511
+ outputDestination: frozenMetadata.outputDestination,
512
+ };
513
+ }
514
+
515
+ /** Native Atlassian credentials remain stored but are not new turn authority. */
516
+ export function personalAtlassianDelegationsFromVisibleConnections(_input: {
517
+ subjectId: string;
518
+ connections: ConnectionMetadata[];
519
+ }): McpPersonalConnectionDelegation[] {
520
+ return [];
521
+ }
522
+
523
+ async function personalGitHubDelegationFromVisibleConnections(input: {
524
+ db: Database;
525
+ accountId: string;
526
+ subjectId: string;
527
+ connections: ConnectionMetadata[];
528
+ resources: ResourceRef[];
529
+ authoritySelection?: McpConnectionAccountSelection;
530
+ }): Promise<McpPersonalConnectionDelegation | null> {
531
+ const resources = personalGitHubRepositoryResources(input.resources);
532
+ if (resources.length === 0) {
533
+ if (input.authoritySelection) {
534
+ throw new Error("personal GitHub connection authority requires selected repositories");
535
+ }
536
+ return null;
537
+ }
538
+ const bindingIds = new Set(resources.map((resource) => resource.credentialBindingId));
539
+ if (bindingIds.size !== 1) {
540
+ throw new Error("accepted work may use only one personal GitHub account");
541
+ }
542
+ const authoritySelection = input.authoritySelection;
543
+ const credentialBindingId = [...bindingIds][0]!;
544
+ const connection = input.connections.find(
545
+ (candidate) =>
546
+ (!authoritySelection || candidate.id === authoritySelection.connectionId) &&
547
+ candidate.subjectId === input.subjectId &&
548
+ candidate.status === "active" &&
549
+ isPersonalGitHubConnection(candidate) &&
550
+ PersonalGitHubConnectionMetadata.safeParse(candidate.metadata).data?.credentialBindingId ===
551
+ credentialBindingId,
552
+ );
553
+ if (
554
+ !connection ||
555
+ !connection.authorityId ||
556
+ connection.grantedScopes.length !== 1 ||
557
+ connection.grantedScopes[0] !== "repo"
558
+ ) {
559
+ throw new Error("personal GitHub connection authority selection is unavailable");
560
+ }
561
+ const metadata = PersonalGitHubConnectionMetadata.parse(connection.metadata);
562
+ if (metadata.credentialBindingId !== credentialBindingId) {
563
+ throw new Error("personal GitHub credential binding does not match the selected connection");
564
+ }
565
+ const selection = await getPersonalGitHubRepositorySelectionState(input.db, {
566
+ accountId: input.accountId,
567
+ originWorkspaceId: connection.workspaceId,
568
+ subjectId: input.subjectId,
569
+ connectionId: connection.id,
570
+ });
571
+ if (
572
+ !selection ||
573
+ selection.selectionGeneration <= 0 ||
574
+ selection.credentialBindingId !== credentialBindingId ||
575
+ selection.providerPrincipalId !== metadata.providerPrincipalId
576
+ ) {
577
+ throw new Error("personal GitHub repository selection changed or is unavailable");
578
+ }
579
+ const selectedById = new Map(
580
+ selection.repositories.map((repository) => [repository.repositoryId, repository]),
581
+ );
582
+ const repositories = resources.map((resource) => {
583
+ const selected = selectedById.get(resource.repositoryId);
584
+ if (
585
+ !selected ||
586
+ selected.canonicalUrl !== resource.uri ||
587
+ (resource.access === "write" && selected.selectedAccess !== "write")
588
+ ) {
589
+ throw new Error("personal GitHub repository resource is outside the selected authority");
590
+ }
591
+ return {
592
+ repositoryId: selected.repositoryId,
593
+ fullName: selected.fullName,
594
+ canonicalUrl: selected.canonicalUrl,
595
+ ref: resource.ref,
596
+ access: resource.access,
597
+ selectionGeneration: selected.selectionGeneration,
598
+ };
599
+ });
600
+ return {
601
+ serverId: PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
602
+ connectionId: connection.id,
603
+ originWorkspaceId: connection.workspaceId,
604
+ ownerSubjectId: input.subjectId,
605
+ providerDomain: "github.com",
606
+ kind: "oauth2",
607
+ connectionType: "github_personal",
608
+ personalGitHubRepositorySelection: {
609
+ credentialBindingId,
610
+ connectionAuthorityGeneration: selection.connectionAuthorityGeneration,
611
+ selectionGeneration: selection.selectionGeneration,
612
+ repositories,
613
+ },
614
+ };
256
615
  }
257
616
 
258
617
  export function personalConnectionDelegationsEqual(
@@ -265,10 +624,14 @@ export function personalConnectionDelegationsEqual(
265
624
  const other = byServer.get(delegation.serverId);
266
625
  return (
267
626
  other?.connectionId === delegation.connectionId &&
627
+ other.canonicalServerId === delegation.canonicalServerId &&
628
+ other.originWorkspaceId === delegation.originWorkspaceId &&
268
629
  other.ownerSubjectId === delegation.ownerSubjectId &&
269
630
  sameProviderDomain(other.providerDomain, delegation.providerDomain) &&
270
631
  other.kind === delegation.kind &&
271
- other.connectionType === delegation.connectionType
632
+ other.connectionType === delegation.connectionType &&
633
+ JSON.stringify(other.personalGitHubRepositorySelection ?? null) ===
634
+ JSON.stringify(delegation.personalGitHubRepositorySelection ?? null)
272
635
  );
273
636
  });
274
637
  }
@@ -278,7 +641,7 @@ export function personalConnectionDelegationForServer(
278
641
  server: Pick<McpServerConfig, "id" | "connectionRef">,
279
642
  ): McpPersonalConnectionDelegation | null {
280
643
  const ref = server.connectionRef;
281
- if (!ref || ref.subjectScope !== "subject") return null;
644
+ if (!isNativeSubjectConnectionRef(ref)) return null;
282
645
  return (
283
646
  delegations.find(
284
647
  (delegation) =>
@@ -318,15 +681,35 @@ export function withFrozenPersonalConnectionDelegations(input: {
318
681
  resolveCredential: ConnectionCredentialResolver;
319
682
  settings: Pick<Settings, "mcpServers">;
320
683
  personalConnectionDelegations: McpPersonalConnectionDelegation[];
684
+ /**
685
+ * Does the frozen delegation owner still hold workspace authority? Supply
686
+ * the canonical `namedSubjectHasLiveWorkspaceAuthority` resolver, never a bare
687
+ * `workspace_memberships` lookup: a managed human's personal workspace has
688
+ * no membership row, so a bare lookup revokes the owner's own connections.
689
+ */
321
690
  ownerHasWorkspaceMembership: (subjectId: string) => Promise<boolean>;
322
691
  }): ConnectionCredentialResolver {
323
692
  return async (request) => {
324
693
  let effectiveRequest = request;
325
- if (request.connectionRef.subjectScope === "subject") {
694
+ const nativeSubjectAuthority = isNativeSubjectConnectionRef(request.connectionRef);
695
+ if (nativeSubjectAuthority) {
326
696
  const config = input.settings.mcpServers.find((server) => server.id === request.serverId);
697
+ const publicationDelegations =
698
+ request.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
699
+ sameProviderDomain(request.connectionRef.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN)
700
+ ? input.personalConnectionDelegations.filter(
701
+ (candidate) =>
702
+ candidate.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
703
+ sameProviderDomain(candidate.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) &&
704
+ candidate.kind === "oauth2" &&
705
+ request.connectionRef.kind === "oauth2",
706
+ )
707
+ : [];
327
708
  const delegation = config
328
709
  ? personalConnectionDelegationForServer(input.personalConnectionDelegations, config)
329
- : null;
710
+ : publicationDelegations.length === 1
711
+ ? publicationDelegations[0]!
712
+ : null;
330
713
  if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
331
714
  return personalAuthorityUnavailable(request);
332
715
  }
@@ -342,26 +725,198 @@ export function withFrozenPersonalConnectionDelegations(input: {
342
725
  };
343
726
  }
344
727
  const result = await input.resolveCredential(effectiveRequest);
345
- if (result.status === "ok" || request.connectionRef.subjectScope !== "subject") {
728
+ if (result.status === "ok" || !nativeSubjectAuthority) {
346
729
  return result;
347
730
  }
348
731
  return personalAuthorityUnavailable(request);
349
732
  };
350
733
  }
351
734
 
352
- export async function freezePersonalConnectionDelegations(input: {
735
+ /** Freeze all native account routes without conflating workspace ownership
736
+ * with the exact causal human required for personal connections. */
737
+ export async function freezeConnectionAccounts(
738
+ input: Parameters<typeof freezePersonalConnectionDelegations>[0] & { accountId: string },
739
+ ): Promise<{
740
+ mcpAccountBindings: McpConnectionAccountBinding[] | null;
741
+ personalConnectionDelegations: McpPersonalConnectionDelegation[];
742
+ }> {
743
+ const selectedIds = new Set(input.tools.map((tool) => tool.id));
744
+ const servers = input.settings.mcpServers.filter(
745
+ (server) =>
746
+ selectedIds.has(server.id) &&
747
+ server.connectionRef &&
748
+ server.connectionRef.authoritySource !== "host",
749
+ );
750
+ const serverIds = new Set(servers.map((server) => server.id));
751
+ let mcpAccountBindings: McpConnectionAccountBinding[];
752
+ if (input.source.kind === "turn") {
753
+ if (input.authoritySelections?.length) {
754
+ throw new ConnectionAccountSelectionError(
755
+ "Agent-created work inherits the exact parent accounts",
756
+ { version: 1, reason: "parent_accounts_required", accounts: [] },
757
+ );
758
+ }
759
+ const inherited = await getSessionTurnMcpAccountBindings(
760
+ input.db,
761
+ input.workspaceId,
762
+ input.source.sessionId,
763
+ input.source.turnId,
764
+ );
765
+ if (inherited === null) {
766
+ return {
767
+ mcpAccountBindings: null,
768
+ personalConnectionDelegations: await freezePersonalConnectionDelegations(input),
769
+ };
770
+ }
771
+ mcpAccountBindings = inherited.filter((binding) => serverIds.has(binding.canonicalServerId));
772
+ } else {
773
+ mcpAccountBindings = mcpAccountBindingsFromVisibleConnections({
774
+ accountId: input.accountId,
775
+ workspaceId: input.workspaceId,
776
+ subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
777
+ servers,
778
+ selectionsFrozen: input.authoritySelectionsFrozen === true,
779
+ connections: await visibleMcpAccountConnections(input.db, {
780
+ accountId: input.accountId,
781
+ workspaceId: input.workspaceId,
782
+ source: input.source,
783
+ }),
784
+ ...(input.authoritySelections
785
+ ? {
786
+ selections: input.authoritySelections.filter((selection) =>
787
+ serverIds.has(selection.serverId),
788
+ ),
789
+ }
790
+ : {}),
791
+ });
792
+ }
793
+ // Dedicated first-party publication/repository/social surfaces keep their
794
+ // existing selection and authority contracts; they are not generic MCP routes.
795
+ const special = await freezePersonalConnectionDelegations({
796
+ ...input,
797
+ settings: {
798
+ ...input.settings,
799
+ mcpServers: input.settings.mcpServers.filter((server) => !serverIds.has(server.id)),
800
+ },
801
+ ...(input.authoritySelections
802
+ ? {
803
+ authoritySelections: input.authoritySelections.filter(
804
+ (selection) => !serverIds.has(selection.serverId),
805
+ ),
806
+ }
807
+ : {}),
808
+ });
809
+ return {
810
+ mcpAccountBindings,
811
+ personalConnectionDelegations: [
812
+ ...personalDelegationsForAccountBindings(mcpAccountBindings),
813
+ ...special,
814
+ ],
815
+ };
816
+ }
817
+
818
+ /** Shared workspace accounts plus the entitled owner's own active accounts. */
819
+ export async function visibleMcpAccountConnections(
820
+ db: Database,
821
+ input: {
822
+ accountId: string;
823
+ workspaceId: string;
824
+ source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
825
+ },
826
+ ): Promise<ConnectionMetadata[]> {
827
+ const personal =
828
+ input.source.kind === "subject" &&
829
+ (await ownerStillBelongsToWorkspace(db, {
830
+ accountId: input.accountId,
831
+ workspaceId: input.workspaceId,
832
+ subjectId: input.source.subjectId,
833
+ }))
834
+ ? await listOwnConnectionMetadata(db, {
835
+ accountId: input.accountId,
836
+ workspaceId: input.workspaceId,
837
+ subjectId: input.source.subjectId,
838
+ })
839
+ : [];
840
+ const workspace = await listConnectionsMetadata(db, input.workspaceId, null);
841
+ return [...workspace.filter((connection) => connection.subjectId === null), ...personal];
842
+ }
843
+
844
+ /**
845
+ * Every account a fresh, selection-free freeze would bind for the generic MCP
846
+ * routes among `tools`: exactly what `freezeConnectionAccounts` binds when no
847
+ * account is chosen. The same entitlement contract applies: `source` is a
848
+ * grant-derived or frozen-owner subject, never a looked-up one. Read-only; it
849
+ * never persists a delegation.
850
+ */
851
+ export async function availableMcpAccountBindings(input: {
353
852
  db: Database;
853
+ accountId: string;
354
854
  workspaceId: string;
355
855
  settings: Pick<Settings, "mcpServers">;
356
856
  tools: ToolRef[];
857
+ source: Exclude<PersonalConnectionDelegationSource, { kind: "turn" }>;
858
+ /** Result of `visibleMcpAccountConnections` for the same source, when already read. */
859
+ connections?: ConnectionMetadata[];
860
+ }): Promise<McpConnectionAccountBinding[]> {
861
+ const selectedIds = new Set(input.tools.map((tool) => tool.id));
862
+ const servers = input.settings.mcpServers.filter(
863
+ (server) =>
864
+ selectedIds.has(server.id) &&
865
+ server.connectionRef &&
866
+ server.connectionRef.authoritySource !== "host",
867
+ );
868
+ if (servers.length === 0) return [];
869
+ return mcpAccountBindingsFromVisibleConnections({
870
+ accountId: input.accountId,
871
+ workspaceId: input.workspaceId,
872
+ subjectId: input.source.kind === "subject" ? input.source.subjectId : null,
873
+ servers,
874
+ connections: input.connections ?? (await visibleMcpAccountConnections(input.db, input)),
875
+ });
876
+ }
877
+
878
+ export async function freezePersonalConnectionDelegations(input: {
879
+ db: Database;
880
+ workspaceId: string;
881
+ settings: Pick<Settings, "mcpServers"> & Partial<Pick<Settings, "githubPersonalOauthEnabled">>;
882
+ tools: ToolRef[];
883
+ resources?: ResourceRef[];
357
884
  source: PersonalConnectionDelegationSource;
885
+ authoritySelections?: McpConnectionAccountSelection[];
886
+ /** Internal durable schedule snapshot, not a default account selector. */
887
+ authoritySelectionsFrozen?: boolean;
888
+ /** Exact first-party export tool + permission gate, not broad opengeni attachment. */
889
+ googleDrivePublicationEnabled?: boolean;
890
+ /** Exact first-party Atlassian tool + permission gate. */
891
+ atlassianEnabled?: boolean;
892
+ /** Exact target for successor-mode projection (`session` never crosses it). */
893
+ targetSessionId?: string;
358
894
  }): Promise<McpPersonalConnectionDelegation[]> {
359
895
  const servers = selectedPersonalConnectionServers(input.settings, input.tools);
360
896
  const includeFirstPartyConnections = input.tools.some((tool) => tool.id === "opengeni");
361
- if ((servers.length === 0 && !includeFirstPartyConnections) || input.source.kind === "none") {
897
+ const personalGitHubResources = personalGitHubRepositoryResources(input.resources ?? []);
898
+ if (personalGitHubResources.length > 0 && input.settings.githubPersonalOauthEnabled !== true) {
899
+ throw new Error("personal GitHub repository authority is not enabled");
900
+ }
901
+ if (personalGitHubResources.length > 0 && input.source.kind === "none") {
902
+ throw new Error("personal GitHub repository authority requires an authenticated causal human");
903
+ }
904
+ if (
905
+ servers.length === 0 &&
906
+ !includeFirstPartyConnections &&
907
+ personalGitHubResources.length === 0
908
+ ) {
909
+ return [];
910
+ }
911
+ if (input.source.kind === "none") {
362
912
  return [];
363
913
  }
364
914
  if (input.source.kind === "turn") {
915
+ if ((input.authoritySelections?.length ?? 0) > 0) {
916
+ throw new Error(
917
+ "agent-created work inherits connection authority from its exact parent turn",
918
+ );
919
+ }
365
920
  const inherited = personalConnectionDelegationsFromParent({
366
921
  servers,
367
922
  parentDelegations: await getSessionTurnPersonalConnectionDelegations(
@@ -370,22 +925,99 @@ export async function freezePersonalConnectionDelegations(input: {
370
925
  input.source.sessionId,
371
926
  input.source.turnId,
372
927
  ),
928
+ personalGitHubResources,
929
+ ...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
930
+ });
931
+ return inherited.filter((item) => {
932
+ if (item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
933
+ return input.googleDrivePublicationEnabled === true;
934
+ }
935
+ if (item.connectionType === "atlassian") {
936
+ return false;
937
+ }
938
+ if (item.connectionType === "social") {
939
+ return includeFirstPartyConnections;
940
+ }
941
+ return true;
373
942
  });
374
- return includeFirstPartyConnections
375
- ? inherited
376
- : inherited.filter(
377
- (item) => item.connectionType !== "social" && item.connectionType !== "atlassian",
378
- );
379
943
  }
380
- const membership = await getWorkspaceGrant(input.db, input.source.subjectId, input.workspaceId);
381
- if (!membership) return [];
944
+ const ownerSubjectId = input.source.subjectId;
945
+ const membership = await ownerStillBelongsToWorkspace(input.db, {
946
+ accountId: input.source.accountId,
947
+ workspaceId: input.workspaceId,
948
+ subjectId: ownerSubjectId,
949
+ });
950
+ if (!membership) {
951
+ if ((input.authoritySelections?.length ?? 0) > 0) {
952
+ throw new ConnectionAccountSelectionError(
953
+ "Selected connection account requires live owner workspace access",
954
+ { version: 1, reason: "owner_access_unavailable", accounts: [] },
955
+ );
956
+ }
957
+ if (personalGitHubResources.length > 0) {
958
+ throw new Error("personal GitHub repository authority requires live causal user authority");
959
+ }
960
+ return [];
961
+ }
962
+ const visibleConnections = await listOwnConnectionMetadata(input.db, {
963
+ accountId: input.source.accountId,
964
+ workspaceId: input.workspaceId,
965
+ subjectId: ownerSubjectId,
966
+ });
967
+ const selectedMcpServerIds = new Set(servers.map((server) => server.id));
968
+ const supportedSelectionIds = new Set(selectedMcpServerIds);
969
+ if (input.googleDrivePublicationEnabled) {
970
+ supportedSelectionIds.add(GOOGLE_DRIVE_PUBLICATION_SERVER_ID);
971
+ }
972
+ if (personalGitHubResources.length > 0) {
973
+ supportedSelectionIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
974
+ }
975
+ const unsupportedSelections = (input.authoritySelections ?? []).filter(
976
+ (selection) => !supportedSelectionIds.has(selection.serverId),
977
+ );
978
+ if (unsupportedSelections.length > 0) {
979
+ throw new ConnectionAccountSelectionError(
980
+ `connection authority selection did not match a selected MCP server: ${unsupportedSelections
981
+ .map((selection) => selection.serverId)
982
+ .join(", ")}`,
983
+ {
984
+ version: 1,
985
+ reason: "selected_account_unavailable",
986
+ accounts: unsupportedSelections.map((selection) => ({
987
+ ...selection,
988
+ reason: "connector_unavailable",
989
+ })),
990
+ },
991
+ );
992
+ }
382
993
  const mcp = personalConnectionDelegationsFromVisibleConnections({
383
994
  servers,
384
- subjectId: input.source.subjectId,
385
- connections: await listConnectionsMetadata(input.db, input.workspaceId, input.source.subjectId),
995
+ subjectId: ownerSubjectId,
996
+ connections: visibleConnections,
997
+ authoritySelections: (input.authoritySelections ?? []).filter((selection) =>
998
+ selectedMcpServerIds.has(selection.serverId),
999
+ ),
386
1000
  });
387
- if (!includeFirstPartyConnections) return mcp;
388
- const ownerSubjectId = input.source.subjectId;
1001
+ const personalGitHubAuthoritySelection = (input.authoritySelections ?? []).find(
1002
+ (selection) => selection.serverId === PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
1003
+ );
1004
+ const personalGitHub = await personalGitHubDelegationFromVisibleConnections({
1005
+ db: input.db,
1006
+ accountId: input.source.accountId,
1007
+ subjectId: ownerSubjectId,
1008
+ connections: input.authoritySelectionsFrozen
1009
+ ? visibleConnections.filter(
1010
+ (connection) => connection.id === personalGitHubAuthoritySelection?.connectionId,
1011
+ )
1012
+ : visibleConnections,
1013
+ resources: input.resources ?? [],
1014
+ ...(personalGitHubAuthoritySelection
1015
+ ? { authoritySelection: personalGitHubAuthoritySelection }
1016
+ : {}),
1017
+ });
1018
+ if (!includeFirstPartyConnections) {
1019
+ return [...mcp, ...(personalGitHub ? [personalGitHub] : [])];
1020
+ }
389
1021
  const visible = await listSocialConnections(input.db, input.workspaceId, 500, ownerSubjectId);
390
1022
  const latest = new Map<"x" | "reddit", (typeof visible)[number]>();
391
1023
  for (const connection of visible) {
@@ -399,16 +1031,34 @@ export async function freezePersonalConnectionDelegations(input: {
399
1031
  if (!prior || connection.updatedAt > prior.updatedAt)
400
1032
  latest.set(connection.provider, connection);
401
1033
  }
402
- const personalAtlassian = (
403
- await listConnectionsMetadata(input.db, input.workspaceId, ownerSubjectId)
404
- ).filter(
405
- (connection) =>
406
- connection.subjectId === ownerSubjectId &&
407
- connection.status === "active" &&
408
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
409
- );
1034
+ const personalAtlassian = input.atlassianEnabled
1035
+ ? personalAtlassianDelegationsFromVisibleConnections({
1036
+ subjectId: ownerSubjectId,
1037
+ connections: visibleConnections,
1038
+ })
1039
+ : [];
1040
+ const googleDriveAuthoritySelection = input.googleDrivePublicationEnabled
1041
+ ? (input.authoritySelections ?? []).find(
1042
+ (selection) => selection.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
1043
+ )
1044
+ : undefined;
1045
+ const googleDrivePublication = input.googleDrivePublicationEnabled
1046
+ ? googleDrivePublicationDelegationFromVisibleConnections({
1047
+ subjectId: ownerSubjectId,
1048
+ connections: input.authoritySelectionsFrozen
1049
+ ? visibleConnections.filter(
1050
+ (connection) => connection.id === googleDriveAuthoritySelection?.connectionId,
1051
+ )
1052
+ : visibleConnections,
1053
+ ...(googleDriveAuthoritySelection
1054
+ ? { authoritySelection: googleDriveAuthoritySelection }
1055
+ : {}),
1056
+ })
1057
+ : null;
410
1058
  return [
411
1059
  ...mcp,
1060
+ ...(personalGitHub ? [personalGitHub] : []),
1061
+ ...(googleDrivePublication ? [googleDrivePublication] : []),
412
1062
  ...[...latest.values()].map((connection) => ({
413
1063
  serverId: `social:${connection.provider}`,
414
1064
  connectionId: connection.id,
@@ -417,13 +1067,6 @@ export async function freezePersonalConnectionDelegations(input: {
417
1067
  kind: "oauth2" as const,
418
1068
  connectionType: "social" as const,
419
1069
  })),
420
- ...personalAtlassian.map((connection) => ({
421
- serverId: `atlassian:${connection.id}`,
422
- connectionId: connection.id,
423
- ownerSubjectId,
424
- providerDomain: connection.providerDomain,
425
- kind: connection.kind,
426
- connectionType: "atlassian" as const,
427
- })),
1070
+ ...personalAtlassian,
428
1071
  ];
429
1072
  }