@opengeni/core 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/application/connect-operation.d.ts +3 -0
  3. package/dist/application/sandbox-recovery.d.ts +49 -0
  4. package/dist/{chunk-H7X52RI2.js → chunk-TFHSE7XR.js} +13 -2
  5. package/dist/chunk-TFHSE7XR.js.map +1 -0
  6. package/dist/dependencies.d.ts +1 -1
  7. package/dist/domain/capabilities.d.ts +2 -2
  8. package/dist/domain/connection-account-selection-error.d.ts +6 -0
  9. package/dist/domain/connector-tool-permissions.d.ts +27 -0
  10. package/dist/domain/editable-artifacts/agent-application.d.ts +8 -0
  11. package/dist/domain/environments.d.ts +3 -7
  12. package/dist/domain/insights-timing.d.ts +16 -0
  13. package/dist/domain/insights.d.ts +4 -1
  14. package/dist/domain/knowledge-files.d.ts +1 -1
  15. package/dist/domain/knowledge-preparation.d.ts +9 -0
  16. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  17. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  18. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  19. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  20. package/dist/domain/personal-connection-delegations.d.ts +19 -10
  21. package/dist/domain/pr-review.d.ts +56 -4
  22. package/dist/domain/scheduled-tasks.d.ts +8 -5
  23. package/dist/domain/session-file-access.d.ts +4 -0
  24. package/dist/domain/sessions.d.ts +8 -6
  25. package/dist/domain/skills.d.ts +29 -1
  26. package/dist/editable-artifacts.js +1 -1
  27. package/dist/index.d.ts +7 -5
  28. package/dist/index.js +4868 -4803
  29. package/dist/index.js.map +1 -1
  30. package/dist/sandbox/routing.d.ts +2 -1
  31. package/dist/sandbox/runtime-settings.d.ts +1 -15
  32. package/package.json +11 -19
  33. package/src/application/composer-submit.ts +2 -1
  34. package/src/application/connect-operation.ts +32 -3
  35. package/src/application/sandbox-recovery.ts +55 -0
  36. package/src/application/user-resource-grants.ts +1 -16
  37. package/src/dependencies.ts +5 -1
  38. package/src/domain/capabilities.ts +169 -122
  39. package/src/domain/connection-account-selection-error.ts +9 -0
  40. package/src/domain/connector-tool-permissions.ts +305 -0
  41. package/src/domain/editable-artifacts/agent-application.ts +31 -2
  42. package/src/domain/environments.ts +4 -9
  43. package/src/domain/file-owner.ts +16 -1
  44. package/src/domain/insights-timing.ts +95 -0
  45. package/src/domain/insights.ts +59 -35
  46. package/src/domain/knowledge-files.ts +10 -3
  47. package/src/domain/knowledge-messages.ts +1 -0
  48. package/src/domain/knowledge-preparation.test.ts +251 -0
  49. package/src/domain/knowledge-preparation.ts +126 -0
  50. package/src/domain/mcp-account-bindings.ts +152 -0
  51. package/src/domain/native-mcp-connection-admission.ts +14 -0
  52. package/src/domain/organization-integration-catalog.ts +83 -0
  53. package/src/domain/personal-connection-delegations.ts +226 -177
  54. package/src/domain/pr-review.ts +22 -6
  55. package/src/domain/resources.ts +9 -2
  56. package/src/domain/scheduled-tasks.ts +200 -155
  57. package/src/domain/session-file-access.ts +47 -0
  58. package/src/domain/session-tool-policy.ts +12 -4
  59. package/src/domain/sessions.ts +390 -261
  60. package/src/domain/skills.ts +9 -0
  61. package/src/index.ts +10 -6
  62. package/src/rigs/index.ts +17 -17
  63. package/src/sandbox/routing.ts +18 -7
  64. package/src/sandbox/runtime-settings.ts +7 -91
  65. package/dist/application/host-mcp-owner.d.ts +0 -6
  66. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  67. package/dist/chunk-5WZYFL45.js +0 -266
  68. package/dist/chunk-5WZYFL45.js.map +0 -1
  69. package/dist/chunk-H7X52RI2.js.map +0 -1
  70. package/dist/domain/connection-authority.d.ts +0 -97
  71. package/dist/domain/connection-authority.js +0 -169
  72. package/dist/domain/connection-authority.js.map +0 -1
  73. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  74. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  75. package/dist/domain/packs.d.ts +0 -59
  76. package/dist/domain/product-integration-pack.d.ts +0 -44
  77. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  78. package/dist/remote-mcp-credentials.d.ts +0 -23
  79. package/dist/remote-mcp-credentials.js +0 -11
  80. package/dist/remote-mcp-credentials.js.map +0 -1
  81. package/src/application/host-mcp-owner.ts +0 -41
  82. package/src/application/host-mcp-resolvers.ts +0 -84
  83. package/src/domain/connection-authority.ts +0 -329
  84. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  85. package/src/domain/host-mcp-task-admission.ts +0 -121
  86. package/src/domain/packs.ts +0 -699
  87. package/src/domain/product-integration-pack.ts +0 -42
  88. package/src/domain/product-integration-skill.gen.ts +0 -52
  89. package/src/remote-mcp-credentials.ts +0 -361
@@ -1,21 +1,18 @@
1
+ import { acceptSessionFileAttachments } from "@opengeni/db";
1
2
  import { knowledgeContextForAccess } from "./knowledge";
3
+ import {
4
+ getSessionEvent,
5
+ getSessionRetryReceiptInTransaction,
6
+ retryFailedSessionInTransaction,
7
+ SessionRetryConflictError,
8
+ } from "@opengeni/db";
9
+ import type { SessionRetryRequest, SessionRetryResponse } from "@opengeni/contracts";
2
10
  import { saveAgentLearningSettings } from "@opengeni/db";
3
11
  import { withSessionRlsActorContext } from "@opengeni/db";
4
12
  import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
5
13
  import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
6
14
  import { sessionCreationMetadata } from "../site-session-origin";
7
- import {
8
- hostMcpBindingMatchesSelection,
9
- HostMcpCreateSelections,
10
- type HostMcpCreateSelection,
11
- } from "@opengeni/contracts/host-mcp-bindings";
12
- import {
13
- captureDirectHostMcpAuthority,
14
- HostMcpDelegationAuthorityError,
15
- HostMcpBindingConflictError,
16
- getHostMcpBinding,
17
- getHostMcpDelegation,
18
- } from "@opengeni/db";
15
+
19
16
  import {
20
17
  canonicalizeConfiguredModelId,
21
18
  configuredAllowedModels,
@@ -55,7 +52,8 @@ import {
55
52
  type GoalSpec,
56
53
  type FirstPartyMcpToolName,
57
54
  type McpPersonalConnectionDelegation,
58
- type McpConnectionAuthoritySelection,
55
+ type McpConnectionAccountBinding,
56
+ type McpConnectionAccountSelection,
59
57
  type Permission,
60
58
  type PersonalResourceAttachmentIntent,
61
59
  type ReasoningEffort,
@@ -71,6 +69,7 @@ import {
71
69
  type SessionMcpCredentialUpdateInput,
72
70
  type SessionMcpServerInput,
73
71
  type SessionMcpServerMetadata,
72
+ type SessionMcpApprovalPolicyTarget,
74
73
  type SubmittedTimelineAnnotation,
75
74
  type TimelineAnnotation,
76
75
  type UpdateSessionMcpApprovalPolicyResponse,
@@ -101,6 +100,8 @@ import {
101
100
  listDistinctVariableSetSelectionsInGroup,
102
101
  listDistinctRigVersionIdsInGroup,
103
102
  listInstalledPortableSkills,
103
+ listEnabledMcpCapabilityServers,
104
+ requireApprovalWithFloor,
104
105
  getSandbox,
105
106
  getSession,
106
107
  getInitializedSessionCreateReplay,
@@ -153,7 +154,7 @@ import { hasPermission, requirePermission, type AccessGrantAuthorization } from
153
154
  import { externalCreationMetadata } from "./external-creation-attribution";
154
155
  import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
155
156
  import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
156
- import { prepareHostMcpOwnerAuthorization } from "../application/host-mcp-owner";
157
+
157
158
  import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
158
159
  import type {
159
160
  AcceptSessionUserMessageDependencies,
@@ -166,7 +167,7 @@ import {
166
167
  requireSessionAuthorization,
167
168
  SessionAuthorizationDeniedError,
168
169
  } from "../session-authorization";
169
- import { assertHostMcpAuthoritySourceAdmissionEnabled } from "./host-mcp-authority-source-admission";
170
+ import { assertNativeMcpConnectionRef } from "./native-mcp-connection-admission";
170
171
  import {
171
172
  preflightCreateTimeSandboxTarget,
172
173
  swapActiveSandbox,
@@ -183,7 +184,7 @@ import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
183
184
  import { validateSubmittedTimelineAnnotations } from "./timeline-annotations";
184
185
  import { requireVariableSetEncryption, validateVariableSetAttachment } from "./environments";
185
186
  import {
186
- freezePersonalConnectionDelegations,
187
+ freezeConnectionAccounts,
187
188
  personalConnectionDelegationSourceForGrant,
188
189
  } from "./personal-connection-delegations";
189
190
  import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
@@ -209,6 +210,26 @@ const maxSessionMcpCredentialHeaderValueLength = 4096;
209
210
  // the generic lossy projection cannot silently rewrite this audit fact.
210
211
  const maxToolPolicyAuditRefs = 40;
211
212
 
213
+ function withoutExcludedMcpServers(
214
+ tools: ToolRef[],
215
+ excludedIds: readonly string[] = [],
216
+ ): ToolRef[] {
217
+ const excluded = new Set(excludedIds);
218
+ return tools.filter((tool) => tool.id === "opengeni" || !excluded.has(tool.id));
219
+ }
220
+
221
+ function defaultPolicyExclusions(
222
+ ids: readonly string[] = [],
223
+ ): Pick<SessionToolPolicy, "excludedMcpServerIds"> {
224
+ const sorted = [...new Set(ids)].filter((id) => id !== "opengeni").sort();
225
+ if (sorted.length > 64 || sorted.some((id) => id.length > 200 || !/^[A-Za-z0-9_-]+$/.test(id))) {
226
+ throw new HTTPException(422, {
227
+ message: "connector exclusions must contain at most 64 valid MCP server IDs",
228
+ });
229
+ }
230
+ return sorted.length ? { excludedMcpServerIds: sorted } : {};
231
+ }
232
+
212
233
  function isCatalogOverlayModel(modelId: string | null | undefined): boolean {
213
234
  return (
214
235
  modelId?.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) === true ||
@@ -566,7 +587,7 @@ function validateSessionMcpServersForCreate(
566
587
  const dbServers: CreateSessionMcpServerInput[] = [];
567
588
  const metadata: SessionMcpServerMetadata[] = [];
568
589
  for (const server of servers) {
569
- assertHostMcpAuthoritySourceAdmissionEnabled(settings, server.connectionRef);
590
+ assertNativeMcpConnectionRef(server.connectionRef);
570
591
  if (seenIds.has(server.id)) {
571
592
  throw new HTTPException(422, {
572
593
  message: `duplicate session MCP server id: ${server.id}`,
@@ -760,7 +781,7 @@ export async function createAndStartSessionWithOutcome(input: {
760
781
  captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
761
782
  /** Internal replay identity; the verified caller must also capture authority.
762
783
  * Supplying selection metadata alone never grants runtime use. */
763
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
784
+
764
785
  /** The custom workspace model was frozen by an earlier accepted boundary or
765
786
  * inherited from an existing session, so retirement must not invalidate it. */
766
787
  retainWorkspaceGatewayModel?: boolean;
@@ -838,8 +859,10 @@ export async function createAndStartSessionWithOutcome(input: {
838
859
  // Encrypted DB rows plus matching safe metadata for create-time per-session
839
860
  // MCP servers. Metadata is the only shape emitted in events/responses.
840
861
  mcpServers?: CreateSessionMcpServerInput[];
862
+ mcpApprovalPolicies?: Record<string, SessionMcpApprovalPolicy>;
841
863
  sessionMcpServers?: SessionMcpServerMetadata[];
842
864
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
865
+ mcpAccountBindings?: McpConnectionAccountBinding[] | null;
843
866
  initialPersonalResourceAttachmentIntent?: PersonalResourceAttachmentIntent | null;
844
867
  xaiProviderAccountAuthoritySnapshot?: XaiProviderAccountAuthoritySnapshotV1;
845
868
  // The manager session spawning this worker (a worker-signed sessionId claim
@@ -1034,7 +1057,7 @@ export async function createAndStartSessionWithOutcome(input: {
1034
1057
  toolPolicy: input.toolPolicy,
1035
1058
  metadata: sessionMetadata,
1036
1059
  initialAgentLearning: input.initialAgentLearning,
1037
- selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1060
+
1038
1061
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1039
1062
  ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1040
1063
  createdByActor: input.createdByActor ?? null,
@@ -1060,7 +1083,9 @@ export async function createAndStartSessionWithOutcome(input: {
1060
1083
  sandboxGroupId: input.sandboxGroupId ?? null,
1061
1084
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1062
1085
  mcpServers: input.mcpServers ?? [],
1086
+ mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
1063
1087
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1088
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
1064
1089
  initialPersonalResourceAttachmentIntent:
1065
1090
  input.initialPersonalResourceAttachmentIntent ?? null,
1066
1091
  ...(input.xaiProviderAccountAuthoritySnapshot
@@ -1131,7 +1156,7 @@ export async function createAndStartSessionWithOutcome(input: {
1131
1156
  metadata: sessionMetadata,
1132
1157
  initialAgentLearning: input.initialAgentLearning,
1133
1158
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1134
- selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1159
+
1135
1160
  ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1136
1161
  createdByActor: input.createdByActor ?? null,
1137
1162
  model: input.model,
@@ -1154,7 +1179,9 @@ export async function createAndStartSessionWithOutcome(input: {
1154
1179
  sandboxGroupId: input.sandboxGroupId ?? null,
1155
1180
  ...(input.sandboxOs ? { sandboxOs: input.sandboxOs } : {}),
1156
1181
  mcpServers: input.mcpServers ?? [],
1182
+ mcpApprovalPolicies: input.mcpApprovalPolicies ?? {},
1157
1183
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1184
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
1158
1185
  initialPersonalResourceAttachmentIntent:
1159
1186
  input.initialPersonalResourceAttachmentIntent ?? null,
1160
1187
  ...(input.xaiProviderAccountAuthoritySnapshot
@@ -1325,6 +1352,9 @@ async function finishStartSession(
1325
1352
  ...(input.goal.rootConstraints !== undefined
1326
1353
  ? { rootConstraints: input.goal.rootConstraints }
1327
1354
  : {}),
1355
+ ...(input.goal.reportRequirements !== undefined
1356
+ ? { reportRequirements: input.goal.reportRequirements }
1357
+ : {}),
1328
1358
  ...(input.goal.maxAutoContinuations !== undefined
1329
1359
  ? { maxAutoContinuations: input.goal.maxAutoContinuations }
1330
1360
  : {}),
@@ -1547,7 +1577,8 @@ type PostUserMessageTurnInput = {
1547
1577
  clientEventId?: string;
1548
1578
  mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
1549
1579
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1550
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
1580
+ mcpAccountBindings?: McpConnectionAccountBinding[] | null;
1581
+
1551
1582
  captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
1552
1583
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
1553
1584
  delivery?: "send" | "steer";
@@ -1755,9 +1786,8 @@ export async function postUserMessageTurn(
1755
1786
  ? { recordAgentRunUsage: input.recordAgentRunUsage }
1756
1787
  : {}),
1757
1788
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1758
- ...(input.selectedHostMcpDelegations?.length
1759
- ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
1760
- : {}),
1789
+ mcpAccountBindings: input.mcpAccountBindings ?? null,
1790
+
1761
1791
  ...(input.captureTurnAuthority
1762
1792
  ? { captureTurnAuthority: input.captureTurnAuthority }
1763
1793
  : {}),
@@ -1981,6 +2011,116 @@ async function resolveWorkspaceModelBoundarySettings(
1981
2011
  ).settings;
1982
2012
  }
1983
2013
 
2014
+ /** Explicit recovery, never a new prompt or a Pause/Resume command. */
2015
+ export async function retryFailedSession(
2016
+ deps: ApiRouteDeps,
2017
+ grant: AccessGrant,
2018
+ workspaceId: string,
2019
+ sessionId: string,
2020
+ request: SessionRetryRequest,
2021
+ ): Promise<SessionRetryResponse> {
2022
+ requirePermission(grant, "sessions:control");
2023
+ await requireSessionAuthorization(deps, grant, {
2024
+ sessionId,
2025
+ operation: "session.control",
2026
+ surface: "core",
2027
+ });
2028
+ const replay = await withWorkspaceSubjectSessionActivityRls(
2029
+ deps.db,
2030
+ workspaceId,
2031
+ grant.subjectId,
2032
+ async (db) =>
2033
+ await getSessionRetryReceiptInTransaction(db, {
2034
+ workspaceId,
2035
+ sessionId,
2036
+ subjectId: grant.subjectId,
2037
+ request,
2038
+ }),
2039
+ );
2040
+ if (replay) return replay;
2041
+ const session = await requireSession(deps.db, workspaceId, sessionId);
2042
+ const failure = await getSessionEvent(deps.db, workspaceId, request.failureEventId);
2043
+ if (!failure || failure.sessionId !== sessionId)
2044
+ throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failure event is unavailable");
2045
+ const turn = failure?.turnId ? await getSessionTurn(deps.db, workspaceId, failure.turnId) : null;
2046
+ const retainedModel = turn?.model ?? session.model;
2047
+ const settings = await resolveWorkspaceModelBoundarySettings(
2048
+ deps,
2049
+ grant,
2050
+ workspaceId,
2051
+ [request.model ?? retainedModel],
2052
+ retainedModel,
2053
+ );
2054
+ const model = canonicalConfiguredModel(settings, request.model ?? retainedModel)!;
2055
+ try {
2056
+ assertSessionAllowsProductModel(session, model);
2057
+ } catch (error) {
2058
+ if (error instanceof CodexCompactionV2ProviderLockedError)
2059
+ throw new HTTPException(422, { message: error.message, cause: error });
2060
+ throw error;
2061
+ }
2062
+ await assertWorkspaceModelPolicyAllows(deps.db, settings, workspaceId, model);
2063
+ const executionPolicy = resolveTurnExecutionPolicyV1(settings, {
2064
+ modelId: model,
2065
+ requestedModelId: request.model ?? null,
2066
+ modelSource: request.model === undefined ? "session" : "explicit",
2067
+ reasoningEffort: request.reasoningEffort ?? turn?.reasoningEffort ?? session.reasoningEffort,
2068
+ reasoningSource: request.reasoningEffort === undefined ? "session" : "explicit",
2069
+ latencyMode: request.latencyMode ?? turn?.latencyMode ?? session.latencyMode,
2070
+ latencyModeSource: request.latencyMode === undefined ? "session" : "explicit",
2071
+ });
2072
+ await requireLimit(deps, {
2073
+ accountId: grant.accountId,
2074
+ workspaceId,
2075
+ action: "agent_run:create",
2076
+ quantity: 1,
2077
+ model,
2078
+ });
2079
+ const result = await runIdempotentPersistenceTransaction(
2080
+ {
2081
+ stage: "session.retry",
2082
+ eventTypes: ["turn.recovery.requested", "session.status.changed"],
2083
+ maxAttempts: 3,
2084
+ },
2085
+ async () =>
2086
+ await withWorkspaceSubjectSessionActivityRls(
2087
+ deps.db,
2088
+ workspaceId,
2089
+ grant.subjectId,
2090
+ async (db) =>
2091
+ await retryFailedSessionInTransaction(db, {
2092
+ accountId: grant.accountId,
2093
+ workspaceId,
2094
+ sessionId,
2095
+ subjectId: grant.subjectId,
2096
+ request,
2097
+ executionPolicy,
2098
+ }),
2099
+ ),
2100
+ );
2101
+ // Fanout and workflow dispatch are reconstructible after the durable commit.
2102
+ // Never report an admitted retry as failed because one notification is down.
2103
+ void Promise.all([
2104
+ deps.workflowClient.requestSessionWorkflowWakeDispatch(),
2105
+ (async () => {
2106
+ const events = await Promise.all(
2107
+ result.eventIds.map((id) => getSessionEvent(deps.db, workspaceId, id)),
2108
+ );
2109
+ await publishDurableSessionEvents(
2110
+ deps.bus,
2111
+ workspaceId,
2112
+ sessionId,
2113
+ events.filter((event): event is SessionEvent => event !== null),
2114
+ );
2115
+ })(),
2116
+ ]).catch(() => {
2117
+ console.warn("[sessions] retry notification failed; durable recovery remains", {
2118
+ errorCode: "session_retry_notification_failed",
2119
+ });
2120
+ });
2121
+ return { outcome: result.outcome, turnId: result.turnId, failureEventId: result.failureEventId };
2122
+ }
2123
+
1984
2124
  async function withSessionCreateUsageRecording(input: {
1985
2125
  deps: ApiRouteDeps;
1986
2126
  grant: AccessGrant;
@@ -2027,24 +2167,7 @@ async function createSessionForRequestInFileScope(
2027
2167
  payload.metadata = sessionCreationMetadata(payload.metadata);
2028
2168
  const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
2029
2169
  const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
2030
- const hostSelections = payload.selectedHostMcpDelegations ?? [];
2031
- if (
2032
- hostSelections.length &&
2033
- (!authorization ||
2034
- authorization.grant.accountId !== grant.accountId ||
2035
- authorization.grant.subjectId !== grant.subjectId ||
2036
- authorization?.grant.workspaceId !== workspaceId ||
2037
- !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
2038
- !hasPermission(grant.permissions, "connections:read") ||
2039
- grant.metadata?.["sessionId"] ||
2040
- payload.startMode === "realtime")
2041
- ) {
2042
- throw new HTTPException(403, {
2043
- message: "Host selection requires a verified direct owner and non-realtime start",
2044
- });
2045
- }
2046
- if (hostSelections.length)
2047
- prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
2170
+
2048
2171
  if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
2049
2172
  throw new HTTPException(422, {
2050
2173
  message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
@@ -2193,7 +2316,7 @@ async function createSessionForRequestInFileScope(
2193
2316
  selectedInstalledSkillIds: payload.installedSkillIds ?? [],
2194
2317
  initialAgentLearning: payload.agentLearning,
2195
2318
  ...sessionScope,
2196
- selectedHostMcpDelegations: hostSelections,
2319
+
2197
2320
  ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
2198
2321
  visibility: effectiveVisibility,
2199
2322
  variableSetIds: payload.variableSetIds ?? [],
@@ -2351,6 +2474,23 @@ async function createSessionForRequestInFileScope(
2351
2474
  capabilityRuntimeSettings,
2352
2475
  sessionMcpServers.runtimeServers,
2353
2476
  );
2477
+ const requestedMcpPolicies =
2478
+ payload.mcpApprovalPolicies ?? parentSession?.mcpApprovalPolicies ?? {};
2479
+ const mcpApprovalPolicies: Record<string, SessionMcpApprovalPolicy> = {};
2480
+ if (Object.keys(requestedMcpPolicies).length > 0) {
2481
+ requirePermission(grant, "sessions:control");
2482
+ const inheritedServers = await listEnabledMcpCapabilityServers(db, workspaceId);
2483
+ for (const [id, policy] of Object.entries(requestedMcpPolicies)) {
2484
+ const inherited = inheritedServers.find((server) => server.id === id);
2485
+ if (!inherited || sessionMcpServers.runtimeServers.some((server) => server.id === id)) {
2486
+ throw new HTTPException(422, {
2487
+ message: `MCP approval policy must name an enabled inherited capability: ${id}`,
2488
+ });
2489
+ }
2490
+ mcpApprovalPolicies[id] =
2491
+ requireApprovalWithFloor(policy, inherited.approvalFloor, true) ?? false;
2492
+ }
2493
+ }
2354
2494
  const resources = normalizeResources(
2355
2495
  hasOwnProperty(rawPayload, "resources")
2356
2496
  ? payload.resources
@@ -2395,6 +2535,11 @@ async function createSessionForRequestInFileScope(
2395
2535
  });
2396
2536
  }
2397
2537
  const toolsProvided = hasOwnProperty(rawPayload, "tools");
2538
+ if (toolsProvided && payload.excludedMcpServerIds !== undefined) {
2539
+ throw new HTTPException(422, {
2540
+ message: "connector exclusions require workspace-default tools",
2541
+ });
2542
+ }
2398
2543
  // Visibility became durable draft state after older clients had already
2399
2544
  // written rows without it. Compare it only when the create request supplied
2400
2545
  // the field explicitly; the parsed schema default must not manufacture a
@@ -2417,6 +2562,9 @@ async function createSessionForRequestInFileScope(
2417
2562
  )
2418
2563
  : parentSession.tools,
2419
2564
  runtimeSettings,
2565
+ ).filter(
2566
+ (tool) =>
2567
+ tool.id === "opengeni" || !parentSession.toolPolicy.excludedMcpServerIds?.includes(tool.id),
2420
2568
  );
2421
2569
  if (toolsProvided) {
2422
2570
  assertToolRefsSubset(
@@ -2434,6 +2582,7 @@ async function createSessionForRequestInFileScope(
2434
2582
  toolPolicy = {
2435
2583
  mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
2436
2584
  inheritedFromSessionId: parentSession.id,
2585
+ ...defaultPolicyExclusions(parentSession.toolPolicy.excludedMcpServerIds),
2437
2586
  };
2438
2587
  }
2439
2588
  } else if (toolsProvided) {
@@ -2448,19 +2597,27 @@ async function createSessionForRequestInFileScope(
2448
2597
  );
2449
2598
  toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
2450
2599
  }
2600
+ if (payload.excludedMcpServerIds !== undefined) {
2601
+ if (toolPolicy.mode !== "workspace_default") {
2602
+ throw new HTTPException(403, {
2603
+ message: "connector exclusions require workspace-default tools",
2604
+ });
2605
+ }
2606
+ toolPolicy = {
2607
+ ...toolPolicy,
2608
+ ...defaultPolicyExclusions([
2609
+ ...(toolPolicy.excludedMcpServerIds ?? []),
2610
+ ...payload.excludedMcpServerIds,
2611
+ ]),
2612
+ };
2613
+ }
2614
+ selectedTools = withoutExcludedMcpServers(selectedTools, toolPolicy.excludedMcpServerIds);
2451
2615
  // The first-party MCP server is attached to EVERY session. Registration is
2452
2616
  // independently intersected with the exact model-visible selection and the
2453
2617
  // tool's permission/target authorization predicate, so attachment alone
2454
2618
  // exposes nothing.
2455
2619
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
2456
- const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
2457
- runtimeSettings,
2458
- tools,
2459
- grant,
2460
- workspaceId,
2461
- hostSelections,
2462
- authorization,
2463
- );
2620
+
2464
2621
  const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
2465
2622
  await validateGitHubRepositorySelection(db, workspaceId, resources);
2466
2623
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
@@ -2468,20 +2625,20 @@ async function createSessionForRequestInFileScope(
2468
2625
  message: "object storage is not configured",
2469
2626
  });
2470
2627
  }
2628
+ const attachmentOwnerContext = authorization
2629
+ ? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
2630
+ : grant.principalKind === "agent_attempt"
2631
+ ? await fileOwnerContextForAgent({ db }, grant, "sessions:create")
2632
+ : undefined;
2633
+ const attachmentOwner =
2634
+ attachmentOwnerContext?.privateFileOwnerSubjectId === grant.subjectId ? grant.subjectId : null;
2471
2635
  await validateFileResources(
2472
2636
  db,
2473
2637
  grant.accountId,
2474
2638
  workspaceId,
2475
2639
  personalResourceSubjectId ?? grant.subjectId,
2476
2640
  resources,
2477
- (authorization || grant.principalKind === "agent_attempt") &&
2478
- (effectiveVisibility === "user_private" ||
2479
- sessionScope.memoryScope === "user" ||
2480
- workspace.kind === "personal")
2481
- ? authorization
2482
- ? await fileOwnerContextForAccess({ db }, authorization, "sessions:create")
2483
- : await fileOwnerContextForAgent({ db }, grant, "sessions:create")
2484
- : undefined,
2641
+ attachmentOwnerContext,
2485
2642
  );
2486
2643
  // Every selected Variable Set is independently authorized. Scope does not
2487
2644
  // affect precedence: explicit order is low-to-high and later sets win name
@@ -2513,7 +2670,7 @@ async function createSessionForRequestInFileScope(
2513
2670
  if (payload.rigId) {
2514
2671
  throw new HTTPException(422, {
2515
2672
  message: rig
2516
- ? `rig ${payload.rigId} has no active version to bind`
2673
+ ? `sandbox environment ${payload.rigId} has no active version to bind`
2517
2674
  : `unknown rigId: ${payload.rigId}`,
2518
2675
  });
2519
2676
  }
@@ -2584,6 +2741,9 @@ async function createSessionForRequestInFileScope(
2584
2741
  reasoningEffort,
2585
2742
  latencyMode,
2586
2743
  options: {
2744
+ ...(payload.excludedMcpServerIds !== undefined
2745
+ ? { excludedMcpServerIds: payload.excludedMcpServerIds }
2746
+ : {}),
2587
2747
  ...(visibilityProvided ? { visibility: payload.visibility } : {}),
2588
2748
  ...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
2589
2749
  ...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
@@ -2730,14 +2890,15 @@ async function createSessionForRequestInFileScope(
2730
2890
  const atlassianEnabled =
2731
2891
  firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
2732
2892
  (!firstPartyMcpPermissions?.length || firstPartyMcpPermissions.includes("connections:read"));
2733
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
2893
+ const { personalConnectionDelegations, mcpAccountBindings } = await freezeConnectionAccounts({
2734
2894
  db,
2895
+ accountId: grant.accountId,
2735
2896
  workspaceId,
2736
2897
  settings: runtimeSettings,
2737
2898
  tools,
2738
2899
  resources,
2739
2900
  source: connectionDelegationSource,
2740
- authoritySelections: payload.connectionAuthorities,
2901
+ authoritySelections: payload.connectionAccounts,
2741
2902
  googleDrivePublicationEnabled,
2742
2903
  atlassianEnabled,
2743
2904
  });
@@ -2862,7 +3023,7 @@ async function createSessionForRequestInFileScope(
2862
3023
  throw new HTTPException(422, {
2863
3024
  message: variableSetMismatch
2864
3025
  ? "sandbox:'shared' requires the same variableSet / same environment as the creator's box (the box variable set/environment is fixed at creation); omit sandbox or pass 'new' when attaching a different variableSet/environment."
2865
- : "sandbox:'shared' requires the same rig as the creator's box (the box's rig setup is fixed at creation); omit sandbox or pass 'new' when binding a different rig.",
3026
+ : "sandbox:'shared' requires the same sandbox environment as the creator's box (setup is fixed at creation); omit sandbox or pass 'new' when selecting a different sandbox environment.",
2866
3027
  });
2867
3028
  }
2868
3029
  // Inherited default: deterministic separation on the genuine shared-state
@@ -2925,7 +3086,7 @@ async function createSessionForRequestInFileScope(
2925
3086
  )
2926
3087
  ) {
2927
3088
  throw new HTTPException(422, {
2928
- message: `sandbox group ${sandboxChoice.groupId} runs a different rig (the box's rig setup is fixed at creation); create with the group's rig or omit sandbox for an own box.`,
3089
+ message: `sandbox group ${sandboxChoice.groupId} uses a different sandbox environment (setup is fixed at creation); select the group's sandbox environment or omit sandbox for a separate box.`,
2929
3090
  });
2930
3091
  }
2931
3092
  }
@@ -3118,8 +3279,8 @@ async function createSessionForRequestInFileScope(
3118
3279
  sandboxGroupId,
3119
3280
  metadata: creationMetadata ?? {},
3120
3281
  ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
3121
- selectedHostMcpDelegations: hostSelections,
3122
- ...(captureSelectedHostAuthority || captureLinkedAuthority
3282
+
3283
+ ...(payload.startMode !== "realtime"
3123
3284
  ? {
3124
3285
  captureInitialTurnAuthority: async (
3125
3286
  tx: Database,
@@ -3127,7 +3288,15 @@ async function createSessionForRequestInFileScope(
3127
3288
  turnId: string,
3128
3289
  ) => {
3129
3290
  await captureLinkedAuthority?.(tx, sessionId, turnId);
3130
- await captureSelectedHostAuthority?.(tx, sessionId, turnId);
3291
+ if (attachmentOwner)
3292
+ await acceptSessionFileAttachments(tx, {
3293
+ accountId: grant.accountId,
3294
+ workspaceId,
3295
+ sessionId,
3296
+ turnId,
3297
+ subjectId: attachmentOwner,
3298
+ resources,
3299
+ });
3131
3300
  },
3132
3301
  }
3133
3302
  : {}),
@@ -3156,8 +3325,10 @@ async function createSessionForRequestInFileScope(
3156
3325
  firstPartyMcpPermissions,
3157
3326
  firstPartyMcpTools,
3158
3327
  mcpServers: sessionMcpServers.dbServers,
3328
+ mcpApprovalPolicies,
3159
3329
  sessionMcpServers: sessionMcpServers.metadata,
3160
3330
  personalConnectionDelegations,
3331
+ mcpAccountBindings,
3161
3332
  initialPersonalResourceAttachmentIntent: payload.personalResourceAttachment ?? null,
3162
3333
  workspaceCustomModel: isWorkspaceCustomModelId(settings, model),
3163
3334
  retainWorkspaceCustomModel: parentSession !== null && model === inheritedModel,
@@ -3213,18 +3384,7 @@ async function createSessionForRequestInFileScope(
3213
3384
  { message: error.message, cause: error },
3214
3385
  );
3215
3386
  }
3216
- if (error instanceof HostMcpDelegationAuthorityError) {
3217
- throw new HTTPException(403, {
3218
- message: "Host delegation authority unavailable",
3219
- cause: error,
3220
- });
3221
- }
3222
- if (error instanceof HostMcpBindingConflictError) {
3223
- throw new HTTPException(409, {
3224
- message: "Host delegation selection conflicts",
3225
- cause: error,
3226
- });
3227
- }
3387
+
3228
3388
  if (error instanceof AgentCommandAuthorityError) {
3229
3389
  throw new HTTPException(403, { message: error.message });
3230
3390
  }
@@ -3279,89 +3439,6 @@ export async function createSessionForRequest(
3279
3439
  ).session;
3280
3440
  }
3281
3441
 
3282
- /** Snapshot the selected destination, then recheck live actor, binding and grant
3283
- * inside the transaction which accepts the turn. Neither selection nor a
3284
- * registered binding alone is execution authority. Shared by create/send/steer. */
3285
- function prepareSelectedHostTurnAuthority(
3286
- settings: Settings,
3287
- tools: ToolRef[],
3288
- grant: AccessGrant,
3289
- workspaceId: string,
3290
- selections: HostMcpCreateSelection[],
3291
- authorization?: AccessGrantAuthorization,
3292
- ): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
3293
- if (!selections.length) return undefined;
3294
- if (
3295
- !authorization ||
3296
- authorization.grant.accountId !== grant.accountId ||
3297
- authorization.grant.subjectId !== grant.subjectId ||
3298
- authorization?.grant.workspaceId !== workspaceId ||
3299
- !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
3300
- !hasPermission(grant.permissions, "connections:read") ||
3301
- grant.metadata?.["sessionId"]
3302
- )
3303
- throw new HTTPException(403, {
3304
- message: "Host selection requires a verified direct owner",
3305
- });
3306
- const beforeCommit = prepareHostMcpOwnerAuthorization(
3307
- authorization,
3308
- workspaceId,
3309
- "connections:read",
3310
- );
3311
- const configs = new Map(
3312
- selections.map((selection) => {
3313
- const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
3314
- if (
3315
- !configured ||
3316
- !tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
3317
- configured.connectionRef?.authoritySource !== "host" ||
3318
- !configured.connectionRef.hostBinding ||
3319
- !configured.url
3320
- )
3321
- throw new HTTPException(422, {
3322
- message: "Host delegation must match a selected configured host server",
3323
- });
3324
- assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
3325
- return [selection.serverId, structuredClone(configured)] as const;
3326
- }),
3327
- );
3328
- return async (tx, sessionId, turnId) => {
3329
- const owner = await beforeCommit(tx);
3330
- for (const selection of selections) {
3331
- const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
3332
- const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
3333
- const configured = configs.get(selection.serverId)!;
3334
- if (
3335
- !delegation ||
3336
- !binding ||
3337
- delegation.status !== "active" ||
3338
- binding.status !== "active" ||
3339
- delegation.generation !== selection.generation ||
3340
- !hostMcpBindingMatchesSelection(binding, {
3341
- serverId: selection.serverId,
3342
- destinationUrl: configured.url!,
3343
- connectionRef: configured.connectionRef!,
3344
- })
3345
- )
3346
- throw new HTTPException(403, { message: "Host delegation selection changed" });
3347
- try {
3348
- await captureDirectHostMcpAuthority(tx, owner, {
3349
- sessionId,
3350
- turnId,
3351
- delegationId: delegation.id,
3352
- expectedDelegationGeneration: selection.generation,
3353
- });
3354
- } catch (error) {
3355
- if (error instanceof HostMcpDelegationAuthorityError)
3356
- throw new HTTPException(403, { message: "Host delegation authority unavailable" });
3357
- if (error instanceof HostMcpBindingConflictError)
3358
- throw new HTTPException(409, { message: "Host delegation selection conflicts" });
3359
- throw error;
3360
- }
3361
- }
3362
- };
3363
- }
3364
-
3365
3442
  function sessionPromptBoundaryRequestHash(input: {
3366
3443
  delivery: "send" | "steer";
3367
3444
  controlEtag: string | null;
@@ -3376,8 +3453,7 @@ function sessionPromptBoundaryRequestHash(input: {
3376
3453
  latencyMode: "standard" | "priority" | "fast" | null;
3377
3454
  source: "user" | "api";
3378
3455
  mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
3379
- connectionAuthorities?: McpConnectionAuthoritySelection[];
3380
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
3456
+ connectionAccounts?: McpConnectionAccountSelection[];
3381
3457
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
3382
3458
  commandActor: SessionCommandActor;
3383
3459
  }): string {
@@ -3396,10 +3472,7 @@ function sessionPromptBoundaryRequestHash(input: {
3396
3472
  latencyMode: input.latencyMode,
3397
3473
  source: input.source,
3398
3474
  mcpCredentialUpdates: input.mcpCredentialUpdates,
3399
- connectionAuthorities: input.connectionAuthorities ?? [],
3400
- ...(input.selectedHostMcpDelegations?.length
3401
- ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
3402
- : {}),
3475
+ connectionAccounts: input.connectionAccounts ?? [],
3403
3476
  personalResourceAttachment: input.personalResourceAttachment ?? null,
3404
3477
  ...(input.commandActor.type === "service"
3405
3478
  ? {
@@ -3437,8 +3510,7 @@ async function acceptSessionUserMessageInFileScope(
3437
3510
  latencyMode?: "standard" | "priority" | "fast" | null;
3438
3511
  clientEventId?: string;
3439
3512
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
3440
- connectionAuthorities?: McpConnectionAuthoritySelection[];
3441
- selectedHostMcpDelegations?: HostMcpCreateSelection[];
3513
+ connectionAccounts?: McpConnectionAccountSelection[];
3442
3514
  delivery?: "send" | "steer";
3443
3515
  origin?: "human" | "operator";
3444
3516
  controlEtag?: string | null;
@@ -3456,24 +3528,7 @@ async function acceptSessionUserMessageInFileScope(
3456
3528
  replay: boolean;
3457
3529
  }> {
3458
3530
  const { db, bus, workflowClient, objectStorage } = deps;
3459
- const hostSelections = HostMcpCreateSelections.parse(input.selectedHostMcpDelegations ?? []).sort(
3460
- (a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0),
3461
- );
3462
- if (
3463
- hostSelections.length &&
3464
- (!input.authorization ||
3465
- input.authorization.grant.accountId !== grant.accountId ||
3466
- input.authorization.grant.subjectId !== grant.subjectId ||
3467
- input.authorization?.grant.workspaceId !== workspaceId ||
3468
- !hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
3469
- !hasPermission(grant.permissions, "connections:read") ||
3470
- grant.metadata?.["sessionId"])
3471
- )
3472
- throw new HTTPException(403, {
3473
- message: "Host selection requires a verified direct owner",
3474
- });
3475
- if (hostSelections.length)
3476
- prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
3531
+
3477
3532
  const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
3478
3533
  const delivery = input.delivery ?? "send";
3479
3534
  const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
@@ -3511,10 +3566,7 @@ async function acceptSessionUserMessageInFileScope(
3511
3566
  latencyMode: input.latencyMode ?? null,
3512
3567
  source,
3513
3568
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
3514
- selectedHostMcpDelegations: hostSelections,
3515
- ...(input.connectionAuthorities
3516
- ? { connectionAuthorities: input.connectionAuthorities }
3517
- : {}),
3569
+ ...(input.connectionAccounts ? { connectionAccounts: input.connectionAccounts } : {}),
3518
3570
  ...(input.personalResourceAttachment
3519
3571
  ? { personalResourceAttachment: input.personalResourceAttachment }
3520
3572
  : {}),
@@ -3639,21 +3691,18 @@ async function acceptSessionUserMessageInFileScope(
3639
3691
  message: "object storage is not configured",
3640
3692
  });
3641
3693
  }
3694
+ const attachmentOwnerContext = input.authorization
3695
+ ? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
3696
+ : grant.principalKind === "agent_attempt"
3697
+ ? await fileOwnerContextForAgent({ db }, grant, "sessions:control")
3698
+ : undefined;
3642
3699
  await validateFileResources(
3643
3700
  db,
3644
3701
  grant.accountId,
3645
3702
  workspaceId,
3646
3703
  grant.subjectId,
3647
3704
  requestedResources,
3648
- (input.authorization || grant.principalKind === "agent_attempt") &&
3649
- ((await getSessionAuthorityProjection(db, workspaceId, sessionId))?.visibility ===
3650
- "user_private" ||
3651
- existingSession.memoryScope === "user" ||
3652
- (await requireWorkspace(db, workspaceId)).kind === "personal")
3653
- ? input.authorization
3654
- ? await fileOwnerContextForAccess({ db }, input.authorization, "sessions:control")
3655
- : await fileOwnerContextForAgent({ db }, grant, "sessions:control")
3656
- : undefined,
3705
+ attachmentOwnerContext,
3657
3706
  );
3658
3707
  await validateGitHubRepositorySelection(db, workspaceId, [
3659
3708
  ...existingSession.resources,
@@ -3675,18 +3724,22 @@ async function acceptSessionUserMessageInFileScope(
3675
3724
  connectionDelegationSource.turnId,
3676
3725
  )
3677
3726
  : null;
3678
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3679
- db,
3680
- workspaceId,
3681
- settings,
3682
- inheritedPersonalConnectionDelegations
3683
- ? {
3684
- personalConnectionDelegations: inheritedPersonalConnectionDelegations,
3685
- }
3686
- : { subjectId: grant.subjectId },
3727
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
3728
+ await settingsWithEnabledCapabilityMcpServers(
3729
+ db,
3730
+ workspaceId,
3731
+ settings,
3732
+ inheritedPersonalConnectionDelegations
3733
+ ? {
3734
+ personalConnectionDelegations: inheritedPersonalConnectionDelegations,
3735
+ }
3736
+ : { subjectId: grant.subjectId },
3737
+ ),
3738
+ existingSession.mcpServers,
3687
3739
  );
3688
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
3740
+ const { personalConnectionDelegations, mcpAccountBindings } = await freezeConnectionAccounts({
3689
3741
  db,
3742
+ accountId: grant.accountId,
3690
3743
  workspaceId,
3691
3744
  settings: runtimeSettings,
3692
3745
  tools: existingSession.tools,
@@ -3703,16 +3756,9 @@ async function acceptSessionUserMessageInFileScope(
3703
3756
  existingSession.firstPartyMcpTools.some((tool) => tool.startsWith("atlassian_")) &&
3704
3757
  (!existingSession.firstPartyMcpPermissions?.length ||
3705
3758
  existingSession.firstPartyMcpPermissions.includes("connections:read")),
3706
- ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
3759
+ ...(input.connectionAccounts ? { authoritySelections: input.connectionAccounts } : {}),
3707
3760
  });
3708
- const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
3709
- settingsWithSessionMcpServerMetadata(runtimeSettings, existingSession.mcpServers),
3710
- existingSession.tools,
3711
- grant,
3712
- workspaceId,
3713
- hostSelections,
3714
- input.authorization,
3715
- );
3761
+
3716
3762
  const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
3717
3763
  const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
3718
3764
  await postUserMessageTurn({
@@ -3735,13 +3781,12 @@ async function acceptSessionUserMessageInFileScope(
3735
3781
  turnExecutionPolicy,
3736
3782
  mcpCredentialUpdates,
3737
3783
  personalConnectionDelegations,
3738
- selectedHostMcpDelegations: hostSelections,
3739
- ...(captureSelectedHostAuthority || captureLinkedAuthority
3784
+ mcpAccountBindings,
3785
+
3786
+ ...(captureLinkedAuthority
3740
3787
  ? {
3741
- captureTurnAuthority: async (tx: Database, turnId: string) => {
3742
- await captureLinkedAuthority?.(tx, sessionId, turnId);
3743
- await captureSelectedHostAuthority?.(tx, sessionId, turnId);
3744
- },
3788
+ captureTurnAuthority: (tx: Database, turnId: string) =>
3789
+ captureLinkedAuthority(tx, sessionId, turnId),
3745
3790
  }
3746
3791
  : {}),
3747
3792
  ...(input.personalResourceAttachment
@@ -3905,7 +3950,7 @@ export async function updateSessionMcpApprovalPolicy(
3905
3950
  });
3906
3951
  requirePermission(grant, "sessions:control");
3907
3952
 
3908
- const outcome: { server?: SessionMcpServerMetadata } = {};
3953
+ const outcome: { server?: SessionMcpApprovalPolicyTarget } = {};
3909
3954
  const events = await appendSessionEventsWithLockedSessionUpdate(
3910
3955
  deps.db,
3911
3956
  grant.workspaceId,
@@ -3976,6 +4021,12 @@ function toolPolicyAuditSnapshot(
3976
4021
  return {
3977
4022
  mode: policy.mode,
3978
4023
  inheritedFromSessionId: policy.inheritedFromSessionId,
4024
+ ...(policy.excludedMcpServerIds?.length
4025
+ ? {
4026
+ excludedMcpServerIds: policy.excludedMcpServerIds.slice(0, maxToolPolicyAuditRefs),
4027
+ excludedMcpServerCount: policy.excludedMcpServerIds.length,
4028
+ }
4029
+ : {}),
3979
4030
  // IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
3980
4031
  toolIds: [...toolRefs]
3981
4032
  .sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`))
@@ -3984,7 +4035,9 @@ function toolPolicyAuditSnapshot(
3984
4035
  toolCount: allToolRefs.length,
3985
4036
  firstPartyMcpTools: [...firstPartyMcpTools].sort(),
3986
4037
  firstPartyMcpToolCount: firstPartyMcpTools.length,
3987
- truncated: allToolRefs.length > toolRefs.length,
4038
+ truncated:
4039
+ allToolRefs.length > toolRefs.length ||
4040
+ (policy.excludedMcpServerIds?.length ?? 0) > maxToolPolicyAuditRefs,
3988
4041
  };
3989
4042
  }
3990
4043
 
@@ -4028,9 +4081,31 @@ export async function updateSessionToolPolicy(
4028
4081
  );
4029
4082
  const explicitRequest = request.mode === "workspace_default" ? null : request;
4030
4083
  const requestedMode = explicitRequest ? "explicit" : "workspace_default";
4084
+ const connectorOnlyEdit =
4085
+ request.mode === "workspace_default" && request.excludedMcpServerIds !== undefined;
4086
+ const requestedExclusions =
4087
+ request.mode === "workspace_default" ? (request.excludedMcpServerIds ?? []) : [];
4031
4088
  const explicitRequestedTools = explicitRequest
4032
4089
  ? (() => {
4033
- const validatedTools = validateToolRefs(explicitRequest.tools, runtimeSettings);
4090
+ // A disconnected stored selection remains policy truth, not executable
4091
+ // authority. Preserve exact existing refs while validating every new or
4092
+ // changed ref against the current runtime registry.
4093
+ const availableIds = new Set(runtimeSettings.mcpServers.map((server) => server.id));
4094
+ const retainedUnavailableRefs = explicitRequest.tools.filter(
4095
+ (tool) =>
4096
+ !availableIds.has(tool.id) &&
4097
+ existingSession.tools.some((existing) => stableJson(existing) === stableJson(tool)),
4098
+ );
4099
+ const retainedIds = new Set(retainedUnavailableRefs.map((tool) => tool.id));
4100
+ const validatedCurrentRefs = validateToolRefs(
4101
+ explicitRequest.tools.filter((tool) => !retainedIds.has(tool.id)),
4102
+ runtimeSettings,
4103
+ );
4104
+ const validatedTools = explicitRequest.tools.filter(
4105
+ (tool) =>
4106
+ retainedUnavailableRefs.includes(tool) ||
4107
+ validatedCurrentRefs.some((validated) => validated.id === tool.id),
4108
+ );
4034
4109
  const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
4035
4110
  const unknown = explicitRequest.tools.find(
4036
4111
  (tool) => !validatedIds.has(`${tool.kind}:${tool.id}`),
@@ -4100,6 +4175,9 @@ export async function updateSessionToolPolicy(
4100
4175
  )
4101
4176
  : parent.tools,
4102
4177
  runtimeSettings,
4178
+ ).filter(
4179
+ (tool) =>
4180
+ tool.id === "opengeni" || !parent.toolPolicy.excludedMcpServerIds?.includes(tool.id),
4103
4181
  );
4104
4182
  const deploymentAllowedFirstPartyMcpTools = new Set(
4105
4183
  deploymentFirstPartyMcpToolPolicy.allowed,
@@ -4119,6 +4197,10 @@ export async function updateSessionToolPolicy(
4119
4197
  nextPolicy = {
4120
4198
  mode: "workspace_default",
4121
4199
  inheritedFromSessionId: parent.id,
4200
+ ...defaultPolicyExclusions([
4201
+ ...(parent.toolPolicy.excludedMcpServerIds ?? []),
4202
+ ...requestedExclusions,
4203
+ ]),
4122
4204
  };
4123
4205
  } else {
4124
4206
  nextTools = explicitRequestedTools!;
@@ -4149,41 +4231,88 @@ export async function updateSessionToolPolicy(
4149
4231
  requestedMode === "workspace_default"
4150
4232
  ? workspaceDefaultFirstPartyTools
4151
4233
  : explicitRequestedFirstPartyTools!;
4152
- nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
4153
- if (agentAttemptCaller) {
4154
- // A human or API key may widen a top-level session; a live agent
4155
- // attempt may only narrow relative to the session's CURRENT
4156
- // effective policy, in either mode. Adopting workspace defaults is a
4157
- // widen whenever it adds a server or tool the session does not hold.
4158
- const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
4159
- const currentEffectiveTools = withFirstPartyTools(
4160
- sessionTracksWorkspaceDefaults
4161
- ? withWorkspaceDefaultMcpTools(
4162
- availableToolRefs(session.tools, runtimeSettings),
4234
+ nextPolicy = {
4235
+ mode: requestedMode,
4236
+ inheritedFromSessionId: null,
4237
+ ...(requestedMode === "workspace_default"
4238
+ ? defaultPolicyExclusions(requestedExclusions)
4239
+ : {}),
4240
+ };
4241
+ }
4242
+ if (connectorOnlyEdit) {
4243
+ if (session.toolPolicy.mode !== "workspace_default") {
4244
+ throw new HTTPException(409, {
4245
+ message: "adopt workspace defaults before editing connector exclusions",
4246
+ });
4247
+ }
4248
+ // A connector switch never rewrites built-in tool choices or selected refs.
4249
+ nextTools = session.tools;
4250
+ nextFirstPartyMcpTools = [
4251
+ ...(session.firstPartyMcpTools ?? deploymentFirstPartyMcpToolPolicy.default),
4252
+ ];
4253
+ }
4254
+ if (!connectorOnlyEdit) {
4255
+ nextTools = withoutExcludedMcpServers(nextTools, nextPolicy.excludedMcpServerIds);
4256
+ }
4257
+ if (agentAttemptCaller && !session.parentSessionId) {
4258
+ // A human or API key may widen a top-level session; a live agent
4259
+ // attempt may only narrow relative to the session's CURRENT
4260
+ // effective policy, in either mode. Adopting workspace defaults is a
4261
+ // widen whenever it adds a server or tool the session does not hold.
4262
+ const sessionTracksWorkspaceDefaults = session.toolPolicy?.mode === "workspace_default";
4263
+ const currentEffectiveTools = withFirstPartyTools(
4264
+ sessionTracksWorkspaceDefaults
4265
+ ? withWorkspaceDefaultMcpTools(
4266
+ availableToolRefs(session.tools, runtimeSettings),
4267
+ deps.settings,
4268
+ runtimeSettings,
4269
+ workspaceSessionToolDefaults,
4270
+ )
4271
+ : session.tools,
4272
+ runtimeSettings,
4273
+ );
4274
+ const currentAllowedTools = withoutExcludedMcpServers(
4275
+ currentEffectiveTools,
4276
+ session.toolPolicy.excludedMcpServerIds,
4277
+ );
4278
+ const nextEffectiveTools =
4279
+ nextPolicy.mode === "workspace_default"
4280
+ ? withFirstPartyTools(
4281
+ withWorkspaceDefaultMcpTools(
4282
+ availableToolRefs(nextTools, runtimeSettings),
4163
4283
  deps.settings,
4164
4284
  runtimeSettings,
4165
4285
  workspaceSessionToolDefaults,
4166
- )
4167
- : session.tools,
4168
- runtimeSettings,
4169
- );
4170
- assertToolRefsSubset(
4171
- nextTools,
4172
- currentEffectiveTools,
4173
- "an agent may only narrow its session tool policy",
4174
- );
4175
- const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
4176
- session.firstPartyMcpTools,
4177
- deploymentFirstPartyMcpToolPolicy,
4178
- );
4179
- const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
4180
- (tool) => !currentFirstPartyCeiling.has(tool),
4181
- );
4182
- if (widenedFirstPartyTool) {
4183
- throw new HTTPException(403, {
4184
- message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`,
4185
- });
4186
- }
4286
+ ),
4287
+ runtimeSettings,
4288
+ )
4289
+ : nextTools;
4290
+ if (
4291
+ nextPolicy.mode === "workspace_default" &&
4292
+ (session.toolPolicy.excludedMcpServerIds ?? []).some(
4293
+ (id) => !nextPolicy.excludedMcpServerIds?.includes(id),
4294
+ )
4295
+ ) {
4296
+ throw new HTTPException(403, {
4297
+ message: "an agent may not remove session connector exclusions",
4298
+ });
4299
+ }
4300
+ assertToolRefsSubset(
4301
+ withoutExcludedMcpServers(nextEffectiveTools, nextPolicy.excludedMcpServerIds),
4302
+ currentAllowedTools,
4303
+ "an agent may only narrow its session tool policy",
4304
+ );
4305
+ const currentFirstPartyCeiling = effectiveFirstPartyMcpToolCeiling(
4306
+ session.firstPartyMcpTools,
4307
+ deploymentFirstPartyMcpToolPolicy,
4308
+ );
4309
+ const widenedFirstPartyTool = nextFirstPartyMcpTools.find(
4310
+ (tool) => !currentFirstPartyCeiling.has(tool),
4311
+ );
4312
+ if (widenedFirstPartyTool) {
4313
+ throw new HTTPException(403, {
4314
+ message: `an agent may only narrow its session OpenGeni tools: ${widenedFirstPartyTool}`,
4315
+ });
4187
4316
  }
4188
4317
  }
4189
4318