@opengeni/core 0.28.1 → 2.1.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 +44 -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 +23 -2
  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 +43 -9
  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 +4973 -1243
  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 +151 -31
  51. package/src/application/new-session-drafts.ts +32 -5
  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 +597 -42
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +144 -6
  80. package/src/domain/scheduled-tasks.ts +504 -77
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +629 -176
  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,27 +2,92 @@ 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,
9
11
  } from "@opengeni/contracts";
10
12
  import {
13
+ GOOGLE_DRIVE_PROVIDER_DOMAIN,
14
+ GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
15
+ GoogleDriveConnectionMetadata,
16
+ googleDriveScopesAllowCapability,
17
+ } from "@opengeni/contracts/google-drive";
18
+ import {
19
+ isPersonalGitHubConnection,
20
+ PERSONAL_GITHUB_CONNECTION_SURFACE_ID,
21
+ PersonalGitHubConnectionMetadata,
22
+ } from "@opengeni/contracts/personal-github";
23
+ import {
24
+ getPersonalGitHubRepositorySelectionState,
11
25
  getSessionTurnPersonalConnectionDelegations,
12
26
  getConnectionMetadata,
13
27
  getSocialConnection,
14
- getWorkspaceGrant,
15
28
  listConnectionsMetadata,
16
29
  listSocialConnections,
30
+ resolvePersonalConnectionAuthoritySelectionOrigin,
31
+ namedSubjectHasLiveWorkspaceAuthority,
17
32
  type Database,
18
33
  type ResolveConnectionCredentialInput,
19
34
  type ResolveConnectionCredentialResult,
20
35
  } from "@opengeni/db";
36
+ import { personalGitHubRepositoryResources } from "./resources";
21
37
 
22
38
  export type PersonalConnectionDelegationSource =
23
- | { 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
+ }
24
49
  | { kind: "turn"; sessionId: string; turnId: string }
25
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
+ }
26
91
  export type AuthorizedSocialConnection = { connection: SocialConnection; subjectId: string | null };
27
92
  export type AuthorizedAtlassianConnection = {
28
93
  connection: ConnectionMetadata;
@@ -47,17 +112,48 @@ export function personalConnectionDelegationSourceForGrant(
47
112
  ): PersonalConnectionDelegationSource {
48
113
  const callerSessionId = grant.metadata?.["sessionId"];
49
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`.
50
125
  if (typeof callerSessionId === "string" && typeof callerTurnId === "string") {
51
126
  return { kind: "turn", sessionId: callerSessionId, turnId: callerTurnId };
52
127
  }
53
128
  if (
54
129
  grant.principalKind === "agent_attempt" ||
55
130
  grant.principalKind === "service" ||
56
- 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
57
153
  ) {
58
154
  return { kind: "none" };
59
155
  }
60
- return { kind: "subject", subjectId: grant.subjectId };
156
+ return { kind: "subject", subjectId: grant.subjectId, accountId: grant.accountId };
61
157
  }
62
158
 
63
159
  export async function authorizedSocialConnectionsForGrant(input: {
@@ -96,7 +192,13 @@ export async function authorizedSocialConnectionsForGrant(input: {
96
192
  ).filter((item) => item.connectionType === "social");
97
193
  const personal: AuthorizedSocialConnection[] = [];
98
194
  for (const delegation of delegations) {
99
- 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
+ )
100
202
  continue;
101
203
  const connection = await getSocialConnection(
102
204
  input.db,
@@ -153,7 +255,13 @@ export async function authorizedAtlassianConnectionsForGrant(input: {
153
255
  ).filter((item) => item.connectionType === "atlassian");
154
256
  const personal: AuthorizedAtlassianConnection[] = [];
155
257
  for (const delegation of delegations) {
156
- 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
+ ) {
157
265
  continue;
158
266
  }
159
267
  const connection = await getConnectionMetadata(
@@ -206,13 +314,19 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
206
314
  servers: McpServerConfig[];
207
315
  subjectId: string;
208
316
  connections: ConnectionMetadata[];
317
+ authoritySelections?: McpConnectionAuthoritySelection[];
318
+ rejectUnselectedActivatedConnections?: boolean;
209
319
  }): McpPersonalConnectionDelegation[] {
210
320
  const delegations: McpPersonalConnectionDelegation[] = [];
211
321
  const connections = canonicalPersonalConnections(input.connections);
322
+ const selections = new Map(
323
+ (input.authoritySelections ?? []).map((selection) => [selection.serverId, selection]),
324
+ );
212
325
  for (const server of input.servers) {
213
326
  const ref = server.connectionRef;
214
327
  if (!ref || ref.subjectScope !== "subject") continue;
215
- const connection = connections.find(
328
+ const selection = selections.get(server.id);
329
+ const eligible = connections.filter(
216
330
  (candidate) =>
217
331
  candidate.subjectId === input.subjectId &&
218
332
  candidate.status === "active" &&
@@ -220,22 +334,69 @@ export function personalConnectionDelegationsFromVisibleConnections(input: {
220
334
  (!ref.kind || candidate.kind === ref.kind) &&
221
335
  (!ref.connectionId || candidate.id === ref.connectionId),
222
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];
223
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
+ }
224
364
  delegations.push({
225
365
  serverId: server.id,
226
366
  connectionId: connection.id,
367
+ ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
227
368
  ownerSubjectId: input.subjectId,
228
369
  providerDomain: connection.providerDomain,
229
370
  kind: connection.kind,
371
+ ...(selection ? { userDelegation: selection.userDelegation } : {}),
230
372
  });
231
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
+ }
232
381
  return delegations;
233
382
  }
234
383
 
235
384
  export function personalConnectionDelegationsFromParent(input: {
236
385
  servers: McpServerConfig[];
237
386
  parentDelegations: McpPersonalConnectionDelegation[];
387
+ targetSessionId?: string;
388
+ rejectActivatedConnections?: boolean;
238
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
+ };
239
400
  const mcp = input.servers.flatMap((server) => {
240
401
  const ref = server.connectionRef;
241
402
  if (!ref || ref.subjectScope !== "subject") return [];
@@ -245,14 +406,233 @@ export function personalConnectionDelegationsFromParent(input: {
245
406
  sameProviderDomain(candidate.providerDomain, ref.providerDomain) &&
246
407
  (!ref.kind || !candidate.kind || candidate.kind === ref.kind),
247
408
  );
248
- return delegation ? [{ ...delegation }] : [];
409
+ return delegation && childEligible(delegation) ? [{ ...delegation }] : [];
249
410
  });
250
- return [
411
+ const projected = [
251
412
  ...mcp,
252
413
  ...input.parentDelegations
253
- .filter((item) => item.connectionType === "social" || item.connectionType === "atlassian")
414
+ .filter(
415
+ (item) =>
416
+ childEligible(item) &&
417
+ (item.connectionType === "social" ||
418
+ item.connectionType === "atlassian" ||
419
+ item.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID),
420
+ )
254
421
  .map((item) => ({ ...item })),
255
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;
432
+ }
433
+
434
+ /**
435
+ * Freezes Google Drive publishing only when one exact subject-owned connection
436
+ * is eligible. Multiple writable Google accounts are intentionally ambiguous:
437
+ * callers must narrow the connection before a later turn can advertise or use
438
+ * the private publication tool.
439
+ */
440
+ export function googleDrivePublicationDelegationFromVisibleConnections(input: {
441
+ subjectId: string;
442
+ connections: ConnectionMetadata[];
443
+ authoritySelection?: McpConnectionAuthoritySelection;
444
+ rejectUnselectedActivatedConnection?: boolean;
445
+ }): McpPersonalConnectionDelegation | null {
446
+ const selection = input.authoritySelection;
447
+ const eligible = input.connections.filter((connection) => {
448
+ if (
449
+ connection.subjectId !== input.subjectId ||
450
+ connection.status !== "active" ||
451
+ connection.kind !== "oauth2" ||
452
+ !sameProviderDomain(connection.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) ||
453
+ !googleDriveScopesAllowCapability(connection.grantedScopes, "publish_file")
454
+ ) {
455
+ return false;
456
+ }
457
+ const metadata = GoogleDriveConnectionMetadata.safeParse(connection.metadata);
458
+ return Boolean(
459
+ metadata.success &&
460
+ metadata.data.outputDestination &&
461
+ metadata.data.lifecycle?.state !== "paused" &&
462
+ (!metadata.data.lifecycle || metadata.data.lifecycle.state === "active"),
463
+ );
464
+ });
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
+ }
498
+ return {
499
+ serverId: GOOGLE_DRIVE_PUBLICATION_SERVER_ID,
500
+ connectionId: connection.id,
501
+ ...(selection ? { originWorkspaceId: connection.workspaceId } : {}),
502
+ ownerSubjectId: input.subjectId,
503
+ providerDomain: connection.providerDomain,
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
+ },
635
+ };
256
636
  }
257
637
 
258
638
  export function personalConnectionDelegationsEqual(
@@ -265,10 +645,15 @@ export function personalConnectionDelegationsEqual(
265
645
  const other = byServer.get(delegation.serverId);
266
646
  return (
267
647
  other?.connectionId === delegation.connectionId &&
648
+ other.originWorkspaceId === delegation.originWorkspaceId &&
268
649
  other.ownerSubjectId === delegation.ownerSubjectId &&
269
650
  sameProviderDomain(other.providerDomain, delegation.providerDomain) &&
270
651
  other.kind === delegation.kind &&
271
- 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)
272
657
  );
273
658
  });
274
659
  }
@@ -318,16 +703,39 @@ export function withFrozenPersonalConnectionDelegations(input: {
318
703
  resolveCredential: ConnectionCredentialResolver;
319
704
  settings: Pick<Settings, "mcpServers">;
320
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
+ */
321
712
  ownerHasWorkspaceMembership: (subjectId: string) => Promise<boolean>;
322
713
  }): ConnectionCredentialResolver {
323
714
  return async (request) => {
324
715
  let effectiveRequest = request;
325
716
  if (request.connectionRef.subjectScope === "subject") {
326
717
  const config = input.settings.mcpServers.find((server) => server.id === request.serverId);
718
+ const publicationDelegations =
719
+ request.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
720
+ sameProviderDomain(request.connectionRef.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN)
721
+ ? input.personalConnectionDelegations.filter(
722
+ (candidate) =>
723
+ candidate.serverId === GOOGLE_DRIVE_PUBLICATION_SERVER_ID &&
724
+ sameProviderDomain(candidate.providerDomain, GOOGLE_DRIVE_PROVIDER_DOMAIN) &&
725
+ candidate.kind === "oauth2" &&
726
+ request.connectionRef.kind === "oauth2",
727
+ )
728
+ : [];
327
729
  const delegation = config
328
730
  ? personalConnectionDelegationForServer(input.personalConnectionDelegations, config)
329
- : null;
330
- if (!delegation || !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId))) {
731
+ : publicationDelegations.length === 1
732
+ ? publicationDelegations[0]!
733
+ : null;
734
+ if (
735
+ !delegation ||
736
+ (!delegation.userDelegation &&
737
+ !(await input.ownerHasWorkspaceMembership(delegation.ownerSubjectId)))
738
+ ) {
331
739
  return personalAuthorityUnavailable(request);
332
740
  }
333
741
  effectiveRequest = {
@@ -352,16 +760,49 @@ export function withFrozenPersonalConnectionDelegations(input: {
352
760
  export async function freezePersonalConnectionDelegations(input: {
353
761
  db: Database;
354
762
  workspaceId: string;
355
- settings: Pick<Settings, "mcpServers">;
763
+ settings: Pick<Settings, "mcpServers"> & Partial<Pick<Settings, "githubPersonalOauthEnabled">>;
356
764
  tools: ToolRef[];
765
+ resources?: ResourceRef[];
357
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;
358
775
  }): Promise<McpPersonalConnectionDelegation[]> {
359
776
  const servers = selectedPersonalConnectionServers(input.settings, input.tools);
360
777
  const includeFirstPartyConnections = input.tools.some((tool) => tool.id === "opengeni");
361
- 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") {
362
793
  return [];
363
794
  }
364
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
+ }
365
806
  const inherited = personalConnectionDelegationsFromParent({
366
807
  servers,
367
808
  parentDelegations: await getSessionTurnPersonalConnectionDelegations(
@@ -370,22 +811,123 @@ export async function freezePersonalConnectionDelegations(input: {
370
811
  input.source.sessionId,
371
812
  input.source.turnId,
372
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;
373
830
  });
374
- return includeFirstPartyConnections
375
- ? inherited
376
- : inherited.filter(
377
- (item) => item.connectionType !== "social" && item.connectionType !== "atlassian",
378
- );
379
831
  }
380
- const membership = await getWorkspaceGrant(input.db, input.source.subjectId, input.workspaceId);
381
- if (!membership) return [];
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(
839
+ input.db,
840
+ input.workspaceId,
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
+ }),
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
+ }
382
904
  const mcp = personalConnectionDelegationsFromVisibleConnections({
383
905
  servers,
384
- subjectId: input.source.subjectId,
385
- connections: await listConnectionsMetadata(input.db, input.workspaceId, input.source.subjectId),
906
+ subjectId: ownerSubjectId,
907
+ connections: visibleConnections,
908
+ authoritySelections: (input.authoritySelections ?? []).filter((selection) =>
909
+ selectedMcpServerIds.has(selection.serverId),
910
+ ),
911
+ ...(input.rejectUnselectedActivatedConnections !== undefined
912
+ ? { rejectUnselectedActivatedConnections: input.rejectUnselectedActivatedConnections }
913
+ : {}),
386
914
  });
387
- if (!includeFirstPartyConnections) return mcp;
388
- 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
+ }
389
931
  const visible = await listSocialConnections(input.db, input.workspaceId, 500, ownerSubjectId);
390
932
  const latest = new Map<"x" | "reddit", (typeof visible)[number]>();
391
933
  for (const connection of visible) {
@@ -399,16 +941,36 @@ export async function freezePersonalConnectionDelegations(input: {
399
941
  if (!prior || connection.updatedAt > prior.updatedAt)
400
942
  latest.set(connection.provider, connection);
401
943
  }
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
- );
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;
410
970
  return [
411
971
  ...mcp,
972
+ ...(personalGitHub ? [personalGitHub] : []),
973
+ ...(googleDrivePublication ? [googleDrivePublication] : []),
412
974
  ...[...latest.values()].map((connection) => ({
413
975
  serverId: `social:${connection.provider}`,
414
976
  connectionId: connection.id,
@@ -417,13 +979,6 @@ export async function freezePersonalConnectionDelegations(input: {
417
979
  kind: "oauth2" as const,
418
980
  connectionType: "social" as const,
419
981
  })),
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
- })),
982
+ ...personalAtlassian,
428
983
  ];
429
984
  }