@opengeni/core 1.0.1 → 2.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/access/index.d.ts +37 -0
  2. package/dist/application/new-session-drafts.d.ts +8 -1
  3. package/dist/application/session-commands.d.ts +18 -11
  4. package/dist/application/session-tenancy.d.ts +18 -0
  5. package/dist/application/user-resource-grants.d.ts +20 -0
  6. package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
  7. package/dist/chunk-H7X52RI2.js.map +1 -0
  8. package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
  9. package/dist/chunk-VCZFFEPB.js.map +1 -0
  10. package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
  11. package/dist/chunk-ZYCNLYGH.js.map +1 -0
  12. package/dist/dependencies.d.ts +12 -1
  13. package/dist/domain/automations.d.ts +51 -0
  14. package/dist/domain/company-brain-governed-writes.d.ts +70 -0
  15. package/dist/domain/company-profile-agent-admin.d.ts +24 -0
  16. package/dist/domain/connection-authority.d.ts +97 -0
  17. package/dist/domain/connection-authority.js +169 -0
  18. package/dist/domain/connection-authority.js.map +1 -0
  19. package/dist/domain/editable-artifacts/service.d.ts +1 -2
  20. package/dist/domain/environments.d.ts +1 -1
  21. package/dist/domain/github-repository-bindings.d.ts +117 -0
  22. package/dist/domain/governed-learning-activation.d.ts +16 -0
  23. package/dist/domain/governed-learning-evaluator.d.ts +6 -0
  24. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  25. package/dist/domain/memory-slack-delivery.d.ts +1 -4
  26. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  27. package/dist/domain/packs.d.ts +5 -1
  28. package/dist/domain/personal-connection-delegations.d.ts +34 -2
  29. package/dist/domain/pr-review.d.ts +149 -0
  30. package/dist/domain/remember.d.ts +69 -0
  31. package/dist/domain/resources.d.ts +22 -1
  32. package/dist/domain/scheduled-tasks.d.ts +43 -2
  33. package/dist/domain/session-tool-policy.d.ts +1 -1
  34. package/dist/domain/sessions.d.ts +40 -6
  35. package/dist/editable-artifact-live/application.d.ts +10 -2
  36. package/dist/editable-artifact-live/ports.d.ts +0 -1
  37. package/dist/editable-artifact-live/server.d.ts +0 -1
  38. package/dist/editable-artifact-live/ticket.d.ts +0 -1
  39. package/dist/editable-artifact-live/types.d.ts +5 -2
  40. package/dist/editable-artifact-live.js +2 -2
  41. package/dist/editable-artifacts.js +2 -4
  42. package/dist/index.d.ts +13 -1
  43. package/dist/index.js +4827 -1164
  44. package/dist/index.js.map +1 -1
  45. package/dist/rigs/index.d.ts +22 -5
  46. package/dist/sandbox/fleet.d.ts +34 -6
  47. package/dist/sandbox/routing.d.ts +15 -1
  48. package/dist/sandbox/runtime-settings.d.ts +39 -0
  49. package/package.json +16 -11
  50. package/src/access/index.ts +157 -31
  51. package/src/application/new-session-drafts.ts +24 -2
  52. package/src/application/session-commands.ts +305 -71
  53. package/src/application/session-tenancy.ts +321 -0
  54. package/src/application/user-resource-grants.ts +140 -0
  55. package/src/billing/limits.ts +25 -15
  56. package/src/dependencies.ts +12 -1
  57. package/src/domain/automations.ts +196 -0
  58. package/src/domain/capabilities.ts +7 -1
  59. package/src/domain/company-brain-governed-writes.ts +330 -0
  60. package/src/domain/company-profile-agent-admin.ts +55 -0
  61. package/src/domain/connection-authority.ts +329 -0
  62. package/src/domain/editable-artifacts/agent-application.ts +17 -9
  63. package/src/domain/editable-artifacts/service.ts +12 -1
  64. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  65. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  66. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  67. package/src/domain/environments.ts +16 -3
  68. package/src/domain/github-repository-bindings.ts +295 -0
  69. package/src/domain/governed-learning-activation.ts +44 -0
  70. package/src/domain/governed-learning-evaluator.ts +49 -0
  71. package/src/domain/governed-learning-slack-publication.ts +157 -0
  72. package/src/domain/insights.ts +9 -0
  73. package/src/domain/memory-slack-delivery.ts +0 -30
  74. package/src/domain/organization-membership-lifecycle.ts +9 -0
  75. package/src/domain/packs.ts +106 -3
  76. package/src/domain/personal-connection-delegations.ts +532 -51
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +131 -4
  80. package/src/domain/scheduled-tasks.ts +497 -76
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +613 -166
  83. package/src/editable-artifact-live/application.ts +21 -3
  84. package/src/editable-artifact-live/ports.ts +0 -1
  85. package/src/editable-artifact-live/server.ts +13 -7
  86. package/src/editable-artifact-live/ticket.ts +1 -5
  87. package/src/editable-artifact-live/types.ts +6 -2
  88. package/src/editable-artifact-live/wire.ts +0 -10
  89. package/src/index.ts +13 -1
  90. package/src/rigs/index.ts +165 -48
  91. package/src/sandbox/fleet.ts +376 -30
  92. package/src/sandbox/routing.ts +91 -29
  93. package/src/sandbox/runtime-settings.ts +274 -0
  94. package/src/session-authorization.ts +17 -49
  95. package/dist/chunk-6WKPWE5S.js.map +0 -1
  96. package/dist/chunk-JJU6I5XD.js.map +0 -1
  97. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  98. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  99. package/src/domain/durable-learning-slack-publication.ts +0 -202
@@ -2,7 +2,9 @@ import type { McpServerConfig, Settings } from "@opengeni/config";
2
2
  import type {
3
3
  AccessGrant,
4
4
  ConnectionMetadata,
5
+ McpConnectionAuthoritySelection,
5
6
  McpPersonalConnectionDelegation,
7
+ ResourceRef,
6
8
  SessionTurn,
7
9
  SocialConnection,
8
10
  ToolRef,
@@ -14,21 +16,78 @@ import {
14
16
  googleDriveScopesAllowCapability,
15
17
  } from "@opengeni/contracts/google-drive";
16
18
  import {
19
+ isPersonalGitHubConnection,
20
+ PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
21
+ PersonalGitHubConnectionMetadata,
22
+ } from "@opengeni/contracts/personal-github";
23
+ import {
24
+ getPersonalGitHubRepositorySelectionState,
17
25
  getSessionTurnPersonalConnectionDelegations,
18
26
  getConnectionMetadata,
19
27
  getSocialConnection,
20
- getWorkspaceGrant,
21
28
  listConnectionsMetadata,
22
29
  listSocialConnections,
30
+ resolvePersonalConnectionAuthoritySelectionOrigin,
31
+ namedSubjectHasLiveWorkspaceAuthority,
23
32
  type Database,
24
33
  type ResolveConnectionCredentialInput,
25
34
  type ResolveConnectionCredentialResult,
26
35
  } from "@opengeni/db";
36
+ import { personalGitHubRepositoryResources } from "./resources";
27
37
 
28
38
  export type PersonalConnectionDelegationSource =
29
- | { kind: "subject"; subjectId: string }
39
+ | {
40
+ kind: "subject";
41
+ subjectId: string;
42
+ /**
43
+ * Organization of the authorizing grant. The owner-only
44
+ * personal-workspace pointer lives on an organization membership, so
45
+ * the account is part of the question, not an optimization.
46
+ */
47
+ accountId: string;
48
+ }
30
49
  | { kind: "turn"; sessionId: string; turnId: string }
31
50
  | { kind: "none" };
51
+
52
+ /**
53
+ * Is `subjectId` still authorized in `workspaceId`?
54
+ *
55
+ * Personal-connection authority has always been "the owner must still belong
56
+ * to this workspace", but the old check was `getWorkspaceGrant` - a bare
57
+ * `workspace_memberships` join. A managed human's personal workspace
58
+ * deliberately has no row in that table (migration 0219 raises on one), so the
59
+ * join answered `false` for the one person who always belongs, and every
60
+ * personal connection silently disappeared inside the owner's own private
61
+ * workspace.
62
+ *
63
+ * `namedSubjectHasLiveWorkspaceAuthority` models both halves - the membership
64
+ * row and the organization membership's `personal_workspace_id` pointer - but
65
+ * it is an **oracle, not an authorization**: it answers for whatever subject it
66
+ * is handed and cannot establish who the caller is (see its doc comment in
67
+ * `@opengeni/db`). Every use is safe only because *this* module has already
68
+ * established entitlement to ask, locally:
69
+ *
70
+ * - at freeze time the subject is `source.subjectId`, and
71
+ * `personalConnectionDelegationSourceForGrant` admits only a grant whose
72
+ * `subjectId` came from a real authenticated principal. Agent-attempt,
73
+ * service, service-initiated, and **delegated/bearer** grants never reach
74
+ * this branch: they resolve to `{kind:"none"}`, or - for a worker-signed
75
+ * agent attempt - to `{kind:"turn"}`, which probes only a frozen
76
+ * `ownerSubjectId` already persisted for the grant's own workspace. Either
77
+ * way an unvalidated signed-token subject is never the probed subject; and
78
+ * - on the `*ForGrant` paths the subject is the frozen `ownerSubjectId` of a
79
+ * delegation already persisted for the workspace the caller is already
80
+ * authorized in.
81
+ *
82
+ * Do not pass any other subject through this helper. A subject that was merely
83
+ * looked up, guessed, or supplied by a caller is not entitled.
84
+ */
85
+ async function ownerStillBelongsToWorkspace(
86
+ db: Database,
87
+ input: { accountId: string; workspaceId: string; subjectId: string },
88
+ ): Promise<boolean> {
89
+ return await namedSubjectHasLiveWorkspaceAuthority(db, input);
90
+ }
32
91
  export type AuthorizedSocialConnection = { connection: SocialConnection; subjectId: string | null };
33
92
  export type AuthorizedAtlassianConnection = {
34
93
  connection: ConnectionMetadata;
@@ -53,17 +112,48 @@ export function personalConnectionDelegationSourceForGrant(
53
112
  ): PersonalConnectionDelegationSource {
54
113
  const callerSessionId = grant.metadata?.["sessionId"];
55
114
  const callerTurnId = grant.metadata?.["turnId"];
115
+ // LOAD-BEARING ORDERING. This turn branch sits ABOVE the delegated filter
116
+ // below, so a grant carrying both `sessionId` and `turnId` never reaches that
117
+ // filter. That is safe only because `DelegatedAccessTokenPayload`'s
118
+ // `superRefine` forbids `principalKind: "human_session"` from carrying any
119
+ // attempt claim (`turnId`/`attemptId`/`executionGeneration`), so a delegated
120
+ // human-session bearer is structurally unable to take this branch. If that
121
+ // refinement is ever relaxed, a delegated bearer could route around the
122
+ // delegated filter by presenting a turn reference - move the filter above
123
+ // this branch at the same time. Pinned by
124
+ // `packages/contracts/test/delegated-access-token-attempt-claims.test.ts`.
56
125
  if (typeof callerSessionId === "string" && typeof callerTurnId === "string") {
57
126
  return { kind: "turn", sessionId: callerSessionId, turnId: callerTurnId };
58
127
  }
59
128
  if (
60
129
  grant.principalKind === "agent_attempt" ||
61
130
  grant.principalKind === "service" ||
62
- grant.serviceInitiator
131
+ grant.serviceInitiator ||
132
+ // A delegated bearer's grant is built entirely from signed token payload
133
+ // (`delegatedAccessContext`); no database row binds its `subjectId` or
134
+ // `workspaceId` to anything. It is therefore never an entitled subject for
135
+ // the owner-only personal-workspace pointer, which CLAUDE.md reserves for
136
+ // the canonical managed-cookie session: "Bearer/delegated principals, API
137
+ // keys, and account or organization administrators receive no
138
+ // personal-workspace access through that exception." Before the pointer
139
+ // existed this was moot - `getWorkspaceGrant` returned null for a personal
140
+ // workspace regardless - so the filter belongs with the pointer.
141
+ //
142
+ // Why testing THIS field is sound while inspecting the others is not: a
143
+ // host signs `subjectId`, `principalKind`, and `serviceInitiator` into the
144
+ // token itself, so those are host-chosen and a hostile host picks whatever
145
+ // it likes. `metadata.delegated` is different - it is stamped by
146
+ // `delegatedAccessContext` in `@opengeni/core` AFTER the token signature is
147
+ // verified, and is not carried in the payload at all, so a host cannot
148
+ // clear it. Do not "harden" this by switching to `principalKind`. The
149
+ // strictly better shape is a positive assertion of how the request
150
+ // authenticated (a canonical managed-cookie stamp) rather than any
151
+ // inspection of the grant; that convergence is tracked separately.
152
+ grant.metadata?.delegated === true
63
153
  ) {
64
154
  return { kind: "none" };
65
155
  }
66
- return { kind: "subject", subjectId: grant.subjectId };
156
+ return { kind: "subject", subjectId: grant.subjectId, accountId: grant.accountId };
67
157
  }
68
158
 
69
159
  export async function authorizedSocialConnectionsForGrant(input: {
@@ -102,7 +192,13 @@ export async function authorizedSocialConnectionsForGrant(input: {
102
192
  ).filter((item) => item.connectionType === "social");
103
193
  const personal: AuthorizedSocialConnection[] = [];
104
194
  for (const delegation of delegations) {
105
- if (!(await getWorkspaceGrant(input.db, delegation.ownerSubjectId, input.grant.workspaceId)))
195
+ if (
196
+ !(await ownerStillBelongsToWorkspace(input.db, {
197
+ accountId: input.grant.accountId,
198
+ workspaceId: input.grant.workspaceId,
199
+ subjectId: delegation.ownerSubjectId,
200
+ }))
201
+ )
106
202
  continue;
107
203
  const connection = await getSocialConnection(
108
204
  input.db,
@@ -159,7 +255,13 @@ export async function authorizedAtlassianConnectionsForGrant(input: {
159
255
  ).filter((item) => item.connectionType === "atlassian");
160
256
  const personal: AuthorizedAtlassianConnection[] = [];
161
257
  for (const delegation of delegations) {
162
- if (!(await getWorkspaceGrant(input.db, delegation.ownerSubjectId, input.grant.workspaceId))) {
258
+ if (
259
+ !(await ownerStillBelongsToWorkspace(input.db, {
260
+ accountId: input.grant.accountId,
261
+ workspaceId: input.grant.workspaceId,
262
+ subjectId: delegation.ownerSubjectId,
263
+ }))
264
+ ) {
163
265
  continue;
164
266
  }
165
267
  const connection = await getConnectionMetadata(
@@ -212,13 +314,19 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
212
314
  servers: McpServerConfig[];
213
315
  subjectId: string;
214
316
  connections: ConnectionMetadata[];
317
+ authoritySelections?: McpConnectionAuthoritySelection[];
318
+ rejectUnselectedActivatedConnections?: boolean;
215
319
  }): McpPersonalConnectionDelegation[] {
216
320
  const delegations: McpPersonalConnectionDelegation[] = [];
217
321
  const connections = canonicalPersonalConnections(input.connections);
322
+ const selections = new Map(
323
+ (input.authoritySelections ?? []).map((selection) => [selection.serverId, selection]),
324
+ );
218
325
  for (const server of input.servers) {
219
326
  const ref = server.connectionRef;
220
327
  if (!ref || ref.subjectScope !== "subject") continue;
221
- const connection = connections.find(
328
+ const selection = selections.get(server.id);
329
+ const eligible = connections.filter(
222
330
  (candidate) =>
223
331
  candidate.subjectId === input.subjectId &&
224
332
  candidate.status === "active" &&
@@ -226,22 +334,69 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
226
334
  (!ref.kind || candidate.kind === ref.kind) &&
227
335
  (!ref.connectionId || candidate.id === ref.connectionId),
228
336
  );
337
+ // An explicit accepted-work selection is authoritative. Canonical newest
338
+ // ordering is only the bounded legacy/omission fallback; it must never
339
+ // replace a caller's exact opaque connection UUID when several accounts
340
+ // share one server/provider tuple.
341
+ const connection = selection
342
+ ? eligible.find((candidate) => candidate.id === selection.connectionId)
343
+ : eligible[0];
229
344
  if (!connection) continue;
345
+ if (connection.authorityId) {
346
+ if (
347
+ !selection ||
348
+ selection.connectionId !== connection.id ||
349
+ selection.userDelegation.authorityId !== connection.authorityId
350
+ ) {
351
+ if (input.rejectUnselectedActivatedConnections) {
352
+ throw new Error(
353
+ `scheduled connection authority selection is required for activated server ${server.id}`,
354
+ );
355
+ }
356
+ // Activated organization-user connections are never admitted through
357
+ // the legacy owner tuple. The caller must select one exact opaque grant.
358
+ continue;
359
+ }
360
+ selections.delete(server.id);
361
+ } else if (selection) {
362
+ throw new Error(`connection authority is not available for legacy server ${server.id}`);
363
+ }
230
364
  delegations.push({
231
365
  serverId: server.id,
232
366
  connectionId: connection.id,
367
+ ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
233
368
  ownerSubjectId: input.subjectId,
234
369
  providerDomain: connection.providerDomain,
235
370
  kind: connection.kind,
371
+ ...(selection ? { userDelegation: selection.userDelegation } : {}),
236
372
  });
237
373
  }
374
+ if (selections.size > 0) {
375
+ throw new Error(
376
+ `connection authority selection did not match an active selected server: ${[
377
+ ...selections.keys(),
378
+ ].join(", ")}`,
379
+ );
380
+ }
238
381
  return delegations;
239
382
  }
240
383
 
241
384
  export function personalConnectionDelegationsFromParent(input: {
242
385
  servers: McpServerConfig[];
243
386
  parentDelegations: McpPersonalConnectionDelegation[];
387
+ targetSessionId?: string;
388
+ rejectActivatedConnections?: boolean;
244
389
  }): McpPersonalConnectionDelegation[] {
390
+ // Every successor is new logical work. `once` remains bound to the original
391
+ // accepted turn, `session` may be re-admitted only into that exact session,
392
+ // and `always` may cross to another authorized session.
393
+ const childEligible = (delegation: McpPersonalConnectionDelegation) => {
394
+ const authority = delegation.userDelegation;
395
+ if (!authority) return true;
396
+ if (authority.mode === "once") return false;
397
+ if (authority.mode === "session") return authority.sessionId === input.targetSessionId;
398
+ return true;
399
+ };
245
400
  const mcp = input.servers.flatMap((server) => {
246
401
  const ref = server.connectionRef;
247
402
  if (!ref || ref.subjectScope !== "subject") return [];
@@ -251,19 +406,29 @@ export function personalConnectionDelegationsFromParent(input: {
251
406
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
252
407
  (!ref.kind || !candidate.kind || candidate.kind === ref.kind),
253
408
  );
254
- return delegation ? [{ ...delegation }] : [];
409
+ return delegation && childEligible(delegation) ? [{ ...delegation }] : [];
255
410
  });
256
- return [
411
+ const projected = [
257
412
  ...mcp,
258
413
  ...input.parentDelegations
259
414
  .filter(
260
415
  (item) =>
261
- item.connectionType === "social" ||
262
- item.connectionType === "atlassian" ||
263
- item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
416
+ childEligible(item) &&
417
+ (item.connectionType === "social" ||
418
+ item.connectionType === "atlassian" ||
419
+ item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID),
264
420
  )
265
421
  .map((item) => ({ ...item })),
266
422
  ];
423
+ if (
424
+ input.rejectActivatedConnections &&
425
+ projected.some((delegation) => delegation.userDelegation)
426
+ ) {
427
+ throw new Error(
428
+ "scheduled connection authority is not available until task occurrence authority is activated",
429
+ );
430
+ }
431
+ return projected;
267
432
  }
268
433
 
269
434
  /**
@@ -275,7 +440,10 @@ export function personalConnectionDelegationsFromParent(input: {
275
440
  export function googleDrivePublicationDelegationFromVisibleConnections(input: {
276
441
  subjectId: string;
277
442
  connections: ConnectionMetadata[];
443
+ authoritySelection?: McpConnectionAuthoritySelection;
444
+ rejectUnselectedActivatedConnection?: boolean;
278
445
  }): McpPersonalConnectionDelegation | null {
446
+ const selection = input.authoritySelection;
279
447
  const eligible = input.connections.filter((connection) => {
280
448
  if (
281
449
  connection.subjectId !== input.subjectId ||
@@ -294,14 +462,176 @@ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
294
462
  (!metadata.data.lifecycle || metadata.data.lifecycle.state === "active"),
295
463
  );
296
464
  });
297
- if (eligible.length !== 1) return null;
298
- const connection = eligible[0]!;
465
+ const selectedEligible = selection
466
+ ? eligible.filter((connection) => connection.id === selection.connectionId)
467
+ : eligible;
468
+ if (selection && selectedEligible.length !== 1) {
469
+ throw new Error("Google Drive publication authority selection is unavailable");
470
+ }
471
+ if (!selection && selectedEligible.length !== 1) return null;
472
+ const connection = selectedEligible[0]!;
473
+ if (connection.authorityId) {
474
+ if (
475
+ !selection ||
476
+ selection.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID ||
477
+ selection.connectionId !== connection.id ||
478
+ selection.userDelegation.authorityId !== connection.authorityId
479
+ ) {
480
+ if (input.rejectUnselectedActivatedConnection) {
481
+ throw new Error(
482
+ "scheduled connection authority selection is required for activated Google Drive publication",
483
+ );
484
+ }
485
+ return null;
486
+ }
487
+ } else if (selection) {
488
+ throw new Error("connection authority is not available for legacy Google Drive publication");
489
+ }
490
+ // Freeze the exact output destination on the accepted delegation: a later
491
+ // connection-settings change must never redirect an already-accepted turn's
492
+ // publication. Eligibility above already proved the metadata parses and the
493
+ // destination exists.
494
+ const frozenMetadata = GoogleDriveConnectionMetadata.parse(connection.metadata);
495
+ if (!frozenMetadata.outputDestination) {
496
+ throw new Error("Google Drive publication destination disappeared during acceptance");
497
+ }
299
498
  return {
300
499
  serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
301
500
  connectionId: connection.id,
501
+ ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
302
502
  ownerSubjectId: input.subjectId,
303
503
  providerDomain: connection.providerDomain,
304
504
  kind: connection.kind,
505
+ ...(selection ? { userDelegation: selection.userDelegation } : {}),
506
+ outputDestination: frozenMetadata.outputDestination,
507
+ };
508
+ }
509
+
510
+ export function personalAtlassianDelegationsFromVisibleConnections(input: {
511
+ subjectId: string;
512
+ connections: ConnectionMetadata[];
513
+ rejectActivatedConnections?: boolean;
514
+ }): McpPersonalConnectionDelegation[] {
515
+ const eligible = input.connections.filter(
516
+ (connection) =>
517
+ connection.subjectId === input.subjectId &&
518
+ connection.status === "active" &&
519
+ sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
520
+ );
521
+ if (input.rejectActivatedConnections && eligible.some((connection) => connection.authorityId)) {
522
+ throw new Error(
523
+ "scheduled connection authority selection is required for activated Atlassian access",
524
+ );
525
+ }
526
+ // Atlassian's first-party multi-call adapter is a named successor. Until it
527
+ // can consume an accepted snapshot, common-authority rows are omitted rather
528
+ // than falling through the legacy owner-tuple resolver.
529
+ return eligible
530
+ .filter((connection) => !connection.authorityId)
531
+ .map((connection) => ({
532
+ serverId: `atlassian:${connection.id}`,
533
+ connectionId: connection.id,
534
+ ownerSubjectId: input.subjectId,
535
+ providerDomain: connection.providerDomain,
536
+ kind: connection.kind,
537
+ connectionType: "atlassian" as const,
538
+ }));
539
+ }
540
+
541
+ async function personalGitHubDelegationFromVisibleConnections(input: {
542
+ db: Database;
543
+ accountId: string;
544
+ subjectId: string;
545
+ connections: ConnectionMetadata[];
546
+ resources: ResourceRef[];
547
+ authoritySelection?: McpConnectionAuthoritySelection;
548
+ }): Promise<McpPersonalConnectionDelegation | null> {
549
+ const resources = personalGitHubRepositoryResources(input.resources);
550
+ if (resources.length === 0) {
551
+ if (input.authoritySelection) {
552
+ throw new Error("personal GitHub connection authority requires selected repositories");
553
+ }
554
+ return null;
555
+ }
556
+ const bindingIds = new Set(resources.map((resource) => resource.credentialBindingId));
557
+ if (bindingIds.size !== 1) {
558
+ throw new Error("accepted work may use only one personal GitHub account");
559
+ }
560
+ const authoritySelection = input.authoritySelection;
561
+ if (!authoritySelection) {
562
+ throw new Error("personal GitHub repository resources require explicit connection authority");
563
+ }
564
+ const connection = input.connections.find(
565
+ (candidate) =>
566
+ candidate.id === authoritySelection.connectionId &&
567
+ candidate.subjectId === input.subjectId &&
568
+ candidate.status === "active" &&
569
+ isPersonalGitHubConnection(candidate),
570
+ );
571
+ if (
572
+ !connection ||
573
+ !connection.authorityId ||
574
+ connection.authorityId !== authoritySelection.userDelegation.authorityId ||
575
+ connection.grantedScopes.length !== 1 ||
576
+ connection.grantedScopes[0] !== "repo"
577
+ ) {
578
+ throw new Error("personal GitHub connection authority selection is unavailable");
579
+ }
580
+ const metadata = PersonalGitHubConnectionMetadata.parse(connection.metadata);
581
+ const credentialBindingId = [...bindingIds][0]!;
582
+ if (metadata.credentialBindingId !== credentialBindingId) {
583
+ throw new Error("personal GitHub credential binding does not match the selected connection");
584
+ }
585
+ const selection = await getPersonalGitHubRepositorySelectionState(input.db, {
586
+ accountId: input.accountId,
587
+ originWorkspaceId: connection.workspaceId,
588
+ subjectId: input.subjectId,
589
+ connectionId: connection.id,
590
+ });
591
+ if (
592
+ !selection ||
593
+ selection.selectionGeneration <= 0 ||
594
+ selection.credentialBindingId !== credentialBindingId ||
595
+ selection.providerPrincipalId !== metadata.providerPrincipalId
596
+ ) {
597
+ throw new Error("personal GitHub repository selection changed or is unavailable");
598
+ }
599
+ const selectedById = new Map(
600
+ selection.repositories.map((repository) => [repository.repositoryId, repository]),
601
+ );
602
+ const repositories = resources.map((resource) => {
603
+ const selected = selectedById.get(resource.repositoryId);
604
+ if (
605
+ !selected ||
606
+ selected.canonicalUrl !== resource.uri ||
607
+ (resource.access === "write" && selected.selectedAccess !== "write")
608
+ ) {
609
+ throw new Error("personal GitHub repository resource is outside the selected authority");
610
+ }
611
+ return {
612
+ repositoryId: selected.repositoryId,
613
+ fullName: selected.fullName,
614
+ canonicalUrl: selected.canonicalUrl,
615
+ ref: resource.ref,
616
+ access: resource.access,
617
+ selectionGeneration: selected.selectionGeneration,
618
+ };
619
+ });
620
+ return {
621
+ serverId: PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
622
+ connectionId: connection.id,
623
+ originWorkspaceId: connection.workspaceId,
624
+ ownerSubjectId: input.subjectId,
625
+ providerDomain: "github.com",
626
+ kind: "oauth2",
627
+ connectionType: "github_personal",
628
+ userDelegation: authoritySelection.userDelegation,
629
+ personalGitHubRepositorySelection: {
630
+ credentialBindingId,
631
+ connectionAuthorityGeneration: selection.connectionAuthorityGeneration,
632
+ selectionGeneration: selection.selectionGeneration,
633
+ repositories,
634
+ },
305
635
  };
306
636
  }
307
637
 
@@ -315,10 +645,15 @@ export function personalConnectionDelegationsEqual(
315
645
  const other = byServer.get(delegation.serverId);
316
646
  return (
317
647
  other?.connectionId === delegation.connectionId &&
648
+ other.originWorkspaceId === delegation.originWorkspaceId &&
318
649
  other.ownerSubjectId === delegation.ownerSubjectId &&
319
650
  sameProviderDomain(other.providerDomain, delegation.providerDomain) &&
320
651
  other.kind === delegation.kind &&
321
- other.connectionType === delegation.connectionType
652
+ other.connectionType === delegation.connectionType &&
653
+ JSON.stringify(other.userDelegation ?? null) ===
654
+ JSON.stringify(delegation.userDelegation ?? null) &&
655
+ JSON.stringify(other.personalGitHubRepositorySelection ?? null) ===
656
+ JSON.stringify(delegation.personalGitHubRepositorySelection ?? null)
322
657
  );
323
658
  });
324
659
  }
@@ -368,6 +703,12 @@ export function withFrozenPersonalConnectionDelegations(input: {
368
703
  resolveCredential: ConnectionCredentialResolver;
369
704
  settings: Pick<Settings, "mcpServers">;
370
705
  personalConnectionDelegations: McpPersonalConnectionDelegation[];
706
+ /**
707
+ * Does the frozen delegation owner still hold workspace authority? Supply
708
+ * the canonical `namedSubjectHasLiveWorkspaceAuthority` resolver, never a bare
709
+ * `workspace_memberships` lookup: a managed human's personal workspace has
710
+ * no membership row, so a bare lookup revokes the owner's own connections.
711
+ */
371
712
  ownerHasWorkspaceMembership: (subjectId: string) => Promise<boolean>;
372
713
  }): ConnectionCredentialResolver {
373
714
  return async (request) => {
@@ -390,7 +731,11 @@ export function withFrozenPersonalConnectionDelegations(input: {
390
731
  : publicationDelegations.length === 1
391
732
  ? publicationDelegations[0]!
392
733
  : null;
393
- if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
734
+ if (
735
+ !delegation ||
736
+ (!delegation.userDelegation &&
737
+ !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId)))
738
+ ) {
394
739
  return personalAuthorityUnavailable(request);
395
740
  }
396
741
  effectiveRequest = {
@@ -415,16 +760,49 @@ export function withFrozenPersonalConnectionDelegations(input: {
415
760
  export async function freezePersonalConnectionDelegations(input: {
416
761
  db: Database;
417
762
  workspaceId: string;
418
- settings: Pick<Settings, "mcpServers">;
763
+ settings: Pick<Settings, "mcpServers"> & Partial<Pick<Settings, "githubPersonalOauthEnabled">>;
419
764
  tools: ToolRef[];
765
+ resources?: ResourceRef[];
420
766
  source: PersonalConnectionDelegationSource;
767
+ authoritySelections?: McpConnectionAuthoritySelection[];
768
+ rejectUnselectedActivatedConnections?: boolean;
769
+ /** Exact first-party export tool + permission gate, not broad opengeni attachment. */
770
+ googleDrivePublicationEnabled?: boolean;
771
+ /** Exact first-party Atlassian tool + permission gate. */
772
+ atlassianEnabled?: boolean;
773
+ /** Exact target for successor-mode projection (`session` never crosses it). */
774
+ targetSessionId?: string;
421
775
  }): Promise<McpPersonalConnectionDelegation[]> {
422
776
  const servers = selectedPersonalConnectionServers(input.settings, input.tools);
423
777
  const includeFirstPartyConnections = input.tools.some((tool) => tool.id === "opengeni");
424
- if ((servers.length === 0 && !includeFirstPartyConnections) || input.source.kind === "none") {
778
+ const personalGitHubResources = personalGitHubRepositoryResources(input.resources ?? []);
779
+ if (personalGitHubResources.length > 0 && input.settings.githubPersonalOauthEnabled !== true) {
780
+ throw new Error("personal GitHub repository authority is not enabled");
781
+ }
782
+ if (personalGitHubResources.length > 0 && input.source.kind === "none") {
783
+ throw new Error("personal GitHub repository authority requires an authenticated causal human");
784
+ }
785
+ if (
786
+ servers.length === 0 &&
787
+ !includeFirstPartyConnections &&
788
+ personalGitHubResources.length === 0
789
+ ) {
790
+ return [];
791
+ }
792
+ if (input.source.kind === "none") {
425
793
  return [];
426
794
  }
427
795
  if (input.source.kind === "turn") {
796
+ if ((input.authoritySelections?.length ?? 0) > 0) {
797
+ throw new Error(
798
+ "agent-created work inherits connection authority from its exact parent turn",
799
+ );
800
+ }
801
+ if (personalGitHubResources.length > 0) {
802
+ throw new Error(
803
+ "agent-created personal GitHub repository authority is not activated in this delivery phase",
804
+ );
805
+ }
428
806
  const inherited = personalConnectionDelegationsFromParent({
429
807
  servers,
430
808
  parentDelegations: await getSessionTurnPersonalConnectionDelegations(
@@ -433,30 +811,123 @@ export async function freezePersonalConnectionDelegations(input: {
433
811
  input.source.sessionId,
434
812
  input.source.turnId,
435
813
  ),
814
+ ...(input.targetSessionId ? { targetSessionId: input.targetSessionId } : {}),
815
+ ...(input.rejectUnselectedActivatedConnections !== undefined
816
+ ? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
817
+ : {}),
818
+ });
819
+ return inherited.filter((item) => {
820
+ if (item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID) {
821
+ return input.googleDrivePublicationEnabled === true;
822
+ }
823
+ if (item.connectionType === "atlassian") {
824
+ return includeFirstPartyConnections && input.atlassianEnabled === true;
825
+ }
826
+ if (item.connectionType === "social") {
827
+ return includeFirstPartyConnections;
828
+ }
829
+ return true;
436
830
  });
437
- return includeFirstPartyConnections
438
- ? inherited
439
- : inherited.filter(
440
- (item) =>
441
- item.connectionType !== "social" &&
442
- item.connectionType !== "atlassian" &&
443
- item.serverId !== GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
444
- );
445
831
  }
446
- const membership = await getWorkspaceGrant(input.db, input.source.subjectId, input.workspaceId);
447
- if (!membership) return [];
448
- const visibleConnections = await listConnectionsMetadata(
832
+ const ownerSubjectId = input.source.subjectId;
833
+ const membership = await ownerStillBelongsToWorkspace(input.db, {
834
+ accountId: input.source.accountId,
835
+ workspaceId: input.workspaceId,
836
+ subjectId: ownerSubjectId,
837
+ });
838
+ const targetLocalConnections = await listConnectionsMetadata(
449
839
  input.db,
450
840
  input.workspaceId,
451
- input.source.subjectId,
841
+ ownerSubjectId,
842
+ );
843
+ const portableSelections = await Promise.all(
844
+ (input.authoritySelections ?? []).map(async (selection) => {
845
+ const originWorkspaceId = await resolvePersonalConnectionAuthoritySelectionOrigin(input.db, {
846
+ accountId: selection.userDelegation.organizationId,
847
+ targetWorkspaceId: input.workspaceId,
848
+ subjectId: ownerSubjectId,
849
+ connectionId: selection.connectionId,
850
+ delegation: selection.userDelegation,
851
+ });
852
+ if (!originWorkspaceId) {
853
+ throw new Error(`connection authority selection is unavailable: ${selection.serverId}`);
854
+ }
855
+ const connection = await getConnectionMetadata(
856
+ input.db,
857
+ originWorkspaceId,
858
+ selection.connectionId,
859
+ ownerSubjectId,
860
+ );
861
+ if (
862
+ !connection ||
863
+ connection.workspaceId !== originWorkspaceId ||
864
+ connection.subjectId !== ownerSubjectId ||
865
+ connection.authorityId !== selection.userDelegation.authorityId
866
+ ) {
867
+ throw new Error(`connection authority selection is unavailable: ${selection.serverId}`);
868
+ }
869
+ return connection;
870
+ }),
452
871
  );
872
+ if (!membership && portableSelections.length === 0) {
873
+ if (personalGitHubResources.length > 0) {
874
+ throw new Error("personal GitHub repository authority requires live causal user authority");
875
+ }
876
+ return [];
877
+ }
878
+ const visibleConnections = [
879
+ ...new Map(
880
+ [...targetLocalConnections, ...portableSelections].map((connection) => [
881
+ connection.id,
882
+ connection,
883
+ ]),
884
+ ).values(),
885
+ ];
886
+ const selectedMcpServerIds = new Set(servers.map((server) => server.id));
887
+ const supportedSelectionIds = new Set(selectedMcpServerIds);
888
+ if (input.googleDrivePublicationEnabled) {
889
+ supportedSelectionIds.add(GOOGLE_DRIVE_PUBLICATION_SERVER_ID);
890
+ }
891
+ if (personalGitHubResources.length > 0) {
892
+ supportedSelectionIds.add(PERSONAL_GITHUB_CONNECTION_SURFACE_ID);
893
+ }
894
+ const unsupportedSelections = (input.authoritySelections ?? []).filter(
895
+ (selection) => !supportedSelectionIds.has(selection.serverId),
896
+ );
897
+ if (unsupportedSelections.length > 0) {
898
+ throw new Error(
899
+ `connection authority selection did not match a selected MCP server: ${unsupportedSelections
900
+ .map((selection) => selection.serverId)
901
+ .join(", ")}`,
902
+ );
903
+ }
453
904
  const mcp = personalConnectionDelegationsFromVisibleConnections({
454
905
  servers,
455
- subjectId: input.source.subjectId,
906
+ subjectId: ownerSubjectId,
456
907
  connections: visibleConnections,
908
+ authoritySelections: (input.authoritySelections ?? []).filter((selection) =>
909
+ selectedMcpServerIds.has(selection.serverId),
910
+ ),
911
+ ...(input.rejectUnselectedActivatedConnections !== undefined
912
+ ? { rejectUnselectedActivatedConnections: input.rejectUnselectedActivatedConnections }
913
+ : {}),
457
914
  });
458
- if (!includeFirstPartyConnections) return mcp;
459
- const ownerSubjectId = input.source.subjectId;
915
+ const personalGitHubAuthoritySelection = (input.authoritySelections ?? []).find(
916
+ (selection) => selection.serverId === PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
917
+ );
918
+ const personalGitHub = await personalGitHubDelegationFromVisibleConnections({
919
+ db: input.db,
920
+ accountId: input.source.accountId,
921
+ subjectId: ownerSubjectId,
922
+ connections: visibleConnections,
923
+ resources: input.resources ?? [],
924
+ ...(personalGitHubAuthoritySelection
925
+ ? { authoritySelection: personalGitHubAuthoritySelection }
926
+ : {}),
927
+ });
928
+ if (!includeFirstPartyConnections) {
929
+ return [...mcp, ...(personalGitHub ? [personalGitHub] : [])];
930
+ }
460
931
  const visible = await listSocialConnections(input.db, input.workspaceId, 500, ownerSubjectId);
461
932
  const latest = new Map<"x" | "reddit", (typeof visible)[number]>();
462
933
  for (const connection of visible) {
@@ -470,18 +941,35 @@ export async function freezePersonalConnectionDelegations(input: {
470
941
  if (!prior || connection.updatedAt > prior.updatedAt)
471
942
  latest.set(connection.provider, connection);
472
943
  }
473
- const personalAtlassian = visibleConnections.filter(
474
- (connection) =>
475
- connection.subjectId === ownerSubjectId &&
476
- connection.status === "active" &&
477
- sameProviderDomain(connection.providerDomain, "api.atlassian.com"),
478
- );
479
- const googleDrivePublication = googleDrivePublicationDelegationFromVisibleConnections({
480
- subjectId: ownerSubjectId,
481
- connections: visibleConnections,
482
- });
944
+ const personalAtlassian = input.atlassianEnabled
945
+ ? personalAtlassianDelegationsFromVisibleConnections({
946
+ subjectId: ownerSubjectId,
947
+ connections: visibleConnections,
948
+ ...(input.rejectUnselectedActivatedConnections !== undefined
949
+ ? { rejectActivatedConnections: input.rejectUnselectedActivatedConnections }
950
+ : {}),
951
+ })
952
+ : [];
953
+ const googleDriveAuthoritySelection = input.googleDrivePublicationEnabled
954
+ ? (input.authoritySelections ?? []).find(
955
+ (selection) => selection.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
956
+ )
957
+ : undefined;
958
+ const googleDrivePublication = input.googleDrivePublicationEnabled
959
+ ? googleDrivePublicationDelegationFromVisibleConnections({
960
+ subjectId: ownerSubjectId,
961
+ connections: visibleConnections,
962
+ ...(googleDriveAuthoritySelection
963
+ ? { authoritySelection: googleDriveAuthoritySelection }
964
+ : {}),
965
+ ...(input.rejectUnselectedActivatedConnections !== undefined
966
+ ? { rejectUnselectedActivatedConnection: input.rejectUnselectedActivatedConnections }
967
+ : {}),
968
+ })
969
+ : null;
483
970
  return [
484
971
  ...mcp,
972
+ ...(personalGitHub ? [personalGitHub] : []),
485
973
  ...(googleDrivePublication ? [googleDrivePublication] : []),
486
974
  ...[...latest.values()].map((connection) => ({
487
975
  serverId: `social:${connection.provider}`,
@@ -491,13 +979,6 @@ export async function freezePersonalConnectionDelegations(input: {
491
979
  kind: "oauth2" as const,
492
980
  connectionType: "social" as const,
493
981
  })),
494
- ...personalAtlassian.map((connection) => ({
495
- serverId: `atlassian:${connection.id}`,
496
- connectionId: connection.id,
497
- ownerSubjectId,
498
- providerDomain: connection.providerDomain,
499
- kind: connection.kind,
500
- connectionType: "atlassian" as const,
501
- })),
982
+ ...personalAtlassian,
502
983
  ];
503
984
  }