@opengeni/core 2.8.3 → 2.9.1-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 (50) hide show
  1. package/dist/access/external-actor-authority.d.ts +47 -0
  2. package/dist/access/index.d.ts +15 -1
  3. package/dist/application/connect-authority.d.ts +13 -0
  4. package/dist/application/connect-operation.d.ts +28 -0
  5. package/dist/application/external-continuation.d.ts +15 -0
  6. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  7. package/dist/application/external-link-work-admission.d.ts +12 -0
  8. package/dist/application/external-workspace-members.d.ts +6 -0
  9. package/dist/application/host-mcp-owner.d.ts +6 -0
  10. package/dist/application/new-session-drafts.d.ts +2 -1
  11. package/dist/application/session-tenancy.d.ts +1 -0
  12. package/dist/dependencies.d.ts +2 -0
  13. package/dist/domain/capabilities.d.ts +19 -2
  14. package/dist/domain/external-creation-attribution.d.ts +6 -0
  15. package/dist/domain/host-mcp-task-admission.d.ts +18 -0
  16. package/dist/domain/product-integration-pack.d.ts +3 -9
  17. package/dist/domain/product-integration-skill.gen.d.ts +5 -0
  18. package/dist/domain/scheduled-tasks.d.ts +3 -0
  19. package/dist/domain/sessions.d.ts +15 -4
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +1231 -825
  22. package/dist/index.js.map +1 -1
  23. package/dist/remote-mcp-credentials.d.ts +8 -0
  24. package/dist/remote-mcp-credentials.js +219 -0
  25. package/dist/remote-mcp-credentials.js.map +1 -0
  26. package/dist/session-authorization.d.ts +5 -6
  27. package/package.json +17 -13
  28. package/src/access/external-actor-authority.ts +94 -0
  29. package/src/access/index.ts +255 -1
  30. package/src/application/connect-authority.ts +77 -0
  31. package/src/application/connect-operation.ts +51 -0
  32. package/src/application/external-continuation.ts +112 -0
  33. package/src/application/external-identity-lifecycle.ts +48 -0
  34. package/src/application/external-link-work-admission.ts +87 -0
  35. package/src/application/external-workspace-members.ts +95 -0
  36. package/src/application/host-mcp-owner.ts +41 -0
  37. package/src/application/new-session-drafts.ts +9 -2
  38. package/src/application/session-tenancy.ts +24 -3
  39. package/src/application/user-resource-grants.ts +2 -2
  40. package/src/dependencies.ts +2 -0
  41. package/src/domain/capabilities.ts +20 -4
  42. package/src/domain/external-creation-attribution.ts +22 -0
  43. package/src/domain/host-mcp-task-admission.ts +111 -0
  44. package/src/domain/product-integration-pack.ts +11 -464
  45. package/src/domain/product-integration-skill.gen.ts +52 -0
  46. package/src/domain/scheduled-tasks.ts +66 -5
  47. package/src/domain/sessions.ts +253 -23
  48. package/src/index.ts +7 -0
  49. package/src/remote-mcp-credentials.ts +293 -0
  50. package/src/session-authorization.ts +22 -17
@@ -1,5 +1,17 @@
1
1
  import { CODEX_MODEL_ID_PREFIX, isCodexBilledModel } from "@opengeni/codex";
2
2
  import { sessionCreationMetadata } from "../site-session-origin";
3
+ import {
4
+ HostMcpBindingDefinition,
5
+ HostMcpCreateSelections,
6
+ type HostMcpCreateSelection,
7
+ } from "@opengeni/contracts/host-mcp-bindings";
8
+ import {
9
+ captureDirectHostMcpAuthority,
10
+ HostMcpDelegationAuthorityError,
11
+ HostMcpBindingConflictError,
12
+ getHostMcpBinding,
13
+ getHostMcpDelegation,
14
+ } from "@opengeni/db";
3
15
  import {
4
16
  canonicalizeConfiguredModelId,
5
17
  configuredAllowedModels,
@@ -46,7 +58,7 @@ import {
46
58
  type Session,
47
59
  type SessionAgentAccess,
48
60
  type SessionCommandReceipt,
49
- type SessionEndUser,
61
+ type SessionScopeSubjectId,
50
62
  type SessionMemoryScope,
51
63
  type SessionSkill,
52
64
  type SessionEvent,
@@ -133,6 +145,10 @@ import {
133
145
  } from "@opengeni/events";
134
146
  import { HTTPException } from "hono/http-exception";
135
147
  import { hasPermission, requirePermission, type AccessGrantAuthorization } from "../access";
148
+ import { externalCreationMetadata } from "./external-creation-attribution";
149
+ import { prepareExternalLinkTurnAdmission } from "../application/external-link-work-admission";
150
+ import { externalContinuationCommitAuthorizer } from "../application/external-continuation";
151
+ import { prepareHostMcpOwnerAuthorization } from "../application/host-mcp-owner";
136
152
  import { recordWorkspaceUsage, requireLimit } from "../billing/limits";
137
153
  import type {
138
154
  AcceptSessionUserMessageDependencies,
@@ -167,7 +183,7 @@ import {
167
183
  } from "./personal-connection-delegations";
168
184
  import { hasReservedOpenGeniSlackBotSessionMetadata } from "./slack-bot";
169
185
  import {
170
- requireCanonicalManagedHuman,
186
+ requireVerifiedOwningUser,
171
187
  requireManagedHumanPrivateSessionCreate,
172
188
  } from "../application/session-tenancy";
173
189
  import {
@@ -213,7 +229,7 @@ async function requireAtomicPersonalResourceAttachment(
213
229
  });
214
230
  }
215
231
  try {
216
- requireCanonicalManagedHuman(authorization, workspaceId);
232
+ requireVerifiedOwningUser(authorization, workspaceId);
217
233
  } catch (error) {
218
234
  throw new HTTPException(403, {
219
235
  message: "Personal resources require the owning managed-human session.",
@@ -729,6 +745,13 @@ export async function createAndStartSessionWithOutcome(input: {
729
745
  /** Internal database-only composition seam. The exact session shell and this
730
746
  * linkage commit together before its first event/turn can be initialized. */
731
747
  beforeCreateCommit?: (tx: Database, sessionId: string) => Promise<void>;
748
+ /** Backend-only accepted-work composition. Recheck live caller authority
749
+ * here before capturing; session-shell authorization may have happened in
750
+ * an earlier transaction. Called only for a newly inserted initial turn. */
751
+ captureInitialTurnAuthority?: (tx: Database, sessionId: string, turnId: string) => Promise<void>;
752
+ /** Internal replay identity; the verified caller must also capture authority.
753
+ * Supplying selection metadata alone never grants runtime use. */
754
+ selectedHostMcpDelegations?: HostMcpCreateSelection[];
732
755
  /** The custom workspace model was frozen by an earlier accepted boundary or
733
756
  * inherited from an existing session, so retirement must not invalidate it. */
734
757
  retainWorkspaceGatewayModel?: boolean;
@@ -801,7 +824,7 @@ export async function createAndStartSessionWithOutcome(input: {
801
824
  // (migration 0427), already resolved against the parent by the caller.
802
825
  // Omitted keeps the workspace defaults for internal lifecycle callers.
803
826
  agentAccess?: SessionAgentAccess;
804
- endUser?: SessionEndUser | null;
827
+ scopeSubjectId?: SessionScopeSubjectId | null;
805
828
  memoryScope?: SessionMemoryScope;
806
829
  // Encrypted DB rows plus matching safe metadata for create-time per-session
807
830
  // MCP servers. Metadata is the only shape emitted in events/responses.
@@ -1001,6 +1024,7 @@ export async function createAndStartSessionWithOutcome(input: {
1001
1024
  tools: input.tools,
1002
1025
  toolPolicy: input.toolPolicy,
1003
1026
  metadata: sessionMetadata,
1027
+ selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1004
1028
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1005
1029
  ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1006
1030
  createdByActor: input.createdByActor ?? null,
@@ -1018,7 +1042,7 @@ export async function createAndStartSessionWithOutcome(input: {
1018
1042
  instructions: input.instructions ?? null,
1019
1043
  policyRole: input.policyRole ?? null,
1020
1044
  ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1021
- ...(input.endUser !== undefined ? { endUser: input.endUser } : {}),
1045
+ ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1022
1046
  ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1023
1047
  parentSessionId: input.parentSessionId ?? null,
1024
1048
  createIdempotencyKey: input.createIdempotencyKey,
@@ -1096,6 +1120,7 @@ export async function createAndStartSessionWithOutcome(input: {
1096
1120
  toolPolicy: input.toolPolicy,
1097
1121
  metadata: sessionMetadata,
1098
1122
  ...(input.createdBy ? { createdBy: input.createdBy } : {}),
1123
+ selectedHostMcpDelegations: input.selectedHostMcpDelegations ?? [],
1099
1124
  ...(frozenCreatedByContext ? { createdByContext: frozenCreatedByContext } : {}),
1100
1125
  createdByActor: input.createdByActor ?? null,
1101
1126
  model: input.model,
@@ -1112,7 +1137,7 @@ export async function createAndStartSessionWithOutcome(input: {
1112
1137
  instructions: input.instructions ?? null,
1113
1138
  policyRole: input.policyRole ?? null,
1114
1139
  ...(input.agentAccess ? { agentAccess: input.agentAccess } : {}),
1115
- ...(input.endUser !== undefined ? { endUser: input.endUser } : {}),
1140
+ ...(input.scopeSubjectId !== undefined ? { scopeSubjectId: input.scopeSubjectId } : {}),
1116
1141
  ...(input.memoryScope ? { memoryScope: input.memoryScope } : {}),
1117
1142
  parentSessionId: input.parentSessionId ?? null,
1118
1143
  sandboxGroupId: input.sandboxGroupId ?? null,
@@ -1168,6 +1193,11 @@ async function finishStartSession(
1168
1193
  bus: EventBus;
1169
1194
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
1170
1195
  initialMessage: string;
1196
+ captureInitialTurnAuthority?: (
1197
+ tx: Database,
1198
+ sessionId: string,
1199
+ turnId: string,
1200
+ ) => Promise<void>;
1171
1201
  deferInitialTurn?: boolean;
1172
1202
  modelContext?: string | null;
1173
1203
  resources: ResourceRef[];
@@ -1253,6 +1283,12 @@ async function finishStartSession(
1253
1283
  accountId: session.accountId,
1254
1284
  workspaceId: session.workspaceId,
1255
1285
  sessionId: session.id,
1286
+ ...(input.captureInitialTurnAuthority
1287
+ ? {
1288
+ captureInitialTurnAuthority: (tx: Database, turnId: string) =>
1289
+ input.captureInitialTurnAuthority!(tx, session.id, turnId),
1290
+ }
1291
+ : {}),
1256
1292
  ...(input.clientEventId ? { clientEventId: input.clientEventId } : {}),
1257
1293
  reasoningEffortFallback: input.reasoningEffort,
1258
1294
  turnExecutionPolicy: input.turnExecutionPolicy,
@@ -1500,6 +1536,8 @@ type PostUserMessageTurnInput = {
1500
1536
  clientEventId?: string;
1501
1537
  mcpCredentialUpdates?: UpdateSessionMcpServerCredentialsInput[];
1502
1538
  personalConnectionDelegations?: McpPersonalConnectionDelegation[];
1539
+ selectedHostMcpDelegations?: HostMcpCreateSelection[];
1540
+ captureTurnAuthority?: (tx: Database, turnId: string) => Promise<void>;
1503
1541
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
1504
1542
  delivery?: "send" | "steer";
1505
1543
  origin?: "human" | "operator";
@@ -1706,6 +1744,12 @@ export async function postUserMessageTurn(
1706
1744
  ? { recordAgentRunUsage: input.recordAgentRunUsage }
1707
1745
  : {}),
1708
1746
  personalConnectionDelegations: input.personalConnectionDelegations ?? [],
1747
+ ...(input.selectedHostMcpDelegations?.length
1748
+ ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
1749
+ : {}),
1750
+ ...(input.captureTurnAuthority
1751
+ ? { captureTurnAuthority: input.captureTurnAuthority }
1752
+ : {}),
1709
1753
  ...(input.personalResourceAttachment
1710
1754
  ? {
1711
1755
  personalResourceAttachment: input.personalResourceAttachment,
@@ -1820,7 +1864,7 @@ export function resolveSessionCreateVisibility(input: {
1820
1864
 
1821
1865
  export type SessionCreateScope = {
1822
1866
  agentAccess: SessionAgentAccess;
1823
- endUser: SessionEndUser | null;
1867
+ scopeSubjectId: SessionScopeSubjectId | null;
1824
1868
  memoryScope: SessionMemoryScope;
1825
1869
  };
1826
1870
 
@@ -1832,7 +1876,6 @@ const AGENT_ACCESS_WIDTH: Record<SessionAgentAccess, number> = {
1832
1876
 
1833
1877
  const MEMORY_SCOPE_WIDTH: Record<SessionMemoryScope, number> = {
1834
1878
  off: 0,
1835
- session: 1,
1836
1879
  user: 2,
1837
1880
  workspace: 3,
1838
1881
  };
@@ -1851,7 +1894,7 @@ export function resolveSessionCreateScope(input: {
1851
1894
  requested: {
1852
1895
  agentAccess: SessionAgentAccess;
1853
1896
  agentAccessProvided: boolean;
1854
- endUser: SessionEndUser | null;
1897
+ scopeSubjectId: SessionScopeSubjectId | null;
1855
1898
  endUserProvided: boolean;
1856
1899
  memoryScope: SessionMemoryScope;
1857
1900
  memoryScopeProvided: boolean;
@@ -1863,7 +1906,7 @@ export function resolveSessionCreateScope(input: {
1863
1906
  if (!parent) {
1864
1907
  resolved = {
1865
1908
  agentAccess: requested.agentAccess,
1866
- endUser: requested.endUser,
1909
+ scopeSubjectId: requested.scopeSubjectId,
1867
1910
  memoryScope: requested.memoryScope,
1868
1911
  };
1869
1912
  } else {
@@ -1875,10 +1918,9 @@ export function resolveSessionCreateScope(input: {
1875
1918
  }
1876
1919
  if (requested.endUserProvided) {
1877
1920
  const same =
1878
- requested.endUser !== null &&
1879
- parent.endUser !== null &&
1880
- requested.endUser.source === parent.endUser.source &&
1881
- requested.endUser.id === parent.endUser.id;
1921
+ requested.scopeSubjectId !== null &&
1922
+ parent.scopeSubjectId !== null &&
1923
+ requested.scopeSubjectId === parent.scopeSubjectId;
1882
1924
  if (!same) {
1883
1925
  throw new HTTPException(403, {
1884
1926
  message: "child end-user label must equal the parent session label",
@@ -1891,10 +1933,10 @@ export function resolveSessionCreateScope(input: {
1891
1933
  message: "child memory scope may only narrow the parent session",
1892
1934
  });
1893
1935
  }
1894
- resolved = { agentAccess, endUser: parent.endUser, memoryScope };
1936
+ resolved = { agentAccess, scopeSubjectId: parent.scopeSubjectId, memoryScope };
1895
1937
  }
1896
- if (resolved.memoryScope === "user" && resolved.endUser === null) {
1897
- throw new HTTPException(422, { message: 'memoryScope "user" requires an endUser label' });
1938
+ if (resolved.memoryScope === "user" && resolved.scopeSubjectId === null) {
1939
+ throw new HTTPException(422, { message: 'memoryScope "user" requires an authenticated user' });
1898
1940
  }
1899
1941
  return resolved;
1900
1942
  }
@@ -1972,6 +2014,26 @@ export async function createSessionForRequestWithOutcome(
1972
2014
  ): Promise<CreateSessionRequestOutcome> {
1973
2015
  const payload = CreateSessionRequest.parse(rawPayload);
1974
2016
  payload.metadata = sessionCreationMetadata(payload.metadata);
2017
+ const creationMetadata = externalCreationMetadata(payload.metadata, authorization, grant);
2018
+ const externalBeforeCreateCommit = externalContinuationCommitAuthorizer(authorization);
2019
+ const hostSelections = payload.selectedHostMcpDelegations ?? [];
2020
+ if (
2021
+ hostSelections.length &&
2022
+ (!authorization ||
2023
+ authorization.grant.accountId !== grant.accountId ||
2024
+ authorization.grant.subjectId !== grant.subjectId ||
2025
+ authorization?.grant.workspaceId !== workspaceId ||
2026
+ !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
2027
+ !hasPermission(grant.permissions, "connections:read") ||
2028
+ grant.metadata?.["sessionId"] ||
2029
+ payload.startMode === "realtime")
2030
+ ) {
2031
+ throw new HTTPException(403, {
2032
+ message: "Host selection requires a verified direct owner and non-realtime start",
2033
+ });
2034
+ }
2035
+ if (hostSelections.length)
2036
+ prepareHostMcpOwnerAuthorization(authorization!, workspaceId, "connections:read");
1975
2037
  if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
1976
2038
  throw new HTTPException(422, {
1977
2039
  message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY} is reserved for scheduler routing`,
@@ -2057,24 +2119,26 @@ export async function createSessionForRequestWithOutcome(
2057
2119
  // visibility: presence is read from the raw request because the Zod
2058
2120
  // defaults erase absent-vs-explicit, and the parent side comes from the
2059
2121
  // durable row rather than anything the caller sent.
2122
+ const creationInitiator = creationInitiatorForGrant(grant);
2123
+ const scopeUser =
2124
+ creationInitiator.initiator?.kind === "subject" ? creationInitiator.initiator.subjectId : null;
2060
2125
  const sessionScope = resolveSessionCreateScope({
2061
2126
  requested: {
2062
2127
  agentAccess: payload.agentAccess,
2063
2128
  agentAccessProvided: hasOwnProperty(rawPayload, "agentAccess"),
2064
- endUser: payload.endUser ?? null,
2065
- endUserProvided: hasOwnProperty(rawPayload, "endUser"),
2129
+ scopeSubjectId: scopeUser && /^(?:user:|external_user:)/u.test(scopeUser) ? scopeUser : null,
2130
+ endUserProvided: false,
2066
2131
  memoryScope: payload.memoryScope,
2067
2132
  memoryScopeProvided: hasOwnProperty(rawPayload, "memoryScope"),
2068
2133
  },
2069
2134
  parent: parentAuthority
2070
2135
  ? {
2071
2136
  agentAccess: parentAuthority.agentAccess,
2072
- endUser: parentAuthority.endUser,
2137
+ scopeSubjectId: parentAuthority.scopeSubjectId,
2073
2138
  memoryScope: parentAuthority.memoryScope,
2074
2139
  }
2075
2140
  : null,
2076
2141
  });
2077
- const creationInitiator = creationInitiatorForGrant(grant);
2078
2142
  const parentCallingTurn =
2079
2143
  parentSession && creationInitiator.actor
2080
2144
  ? await getSessionTurnForAttempt(
@@ -2116,6 +2180,7 @@ export async function createSessionForRequestWithOutcome(
2116
2180
  : {}),
2117
2181
  createIdempotencyKey: payload.idempotencyKey,
2118
2182
  selectedInstalledSkillIds: payload.installedSkillIds ?? [],
2183
+ selectedHostMcpDelegations: hostSelections,
2119
2184
  ...(payload.requestedSessionId ? { requestedSessionId: payload.requestedSessionId } : {}),
2120
2185
  visibility: effectiveVisibility,
2121
2186
  variableSetIds: payload.variableSetIds ?? [],
@@ -2375,6 +2440,15 @@ export async function createSessionForRequestWithOutcome(
2375
2440
  // tool's permission/target authorization predicate, so attachment alone
2376
2441
  // exposes nothing.
2377
2442
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
2443
+ const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
2444
+ runtimeSettings,
2445
+ tools,
2446
+ grant,
2447
+ workspaceId,
2448
+ hostSelections,
2449
+ authorization,
2450
+ );
2451
+ const captureLinkedAuthority = prepareExternalLinkTurnAdmission(authorization);
2378
2452
  await validateGitHubRepositorySelection(db, workspaceId, resources);
2379
2453
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
2380
2454
  throw new HTTPException(503, {
@@ -2975,7 +3049,21 @@ export async function createSessionForRequestWithOutcome(
2975
3049
  // create carries a derived OS; shared spawns inherit the exact parent box.
2976
3050
  ...(effectiveSandboxOs ? { sandboxOs: effectiveSandboxOs } : {}),
2977
3051
  sandboxGroupId,
2978
- metadata: payload.metadata,
3052
+ metadata: creationMetadata ?? {},
3053
+ ...(externalBeforeCreateCommit ? { beforeCreateCommit: externalBeforeCreateCommit } : {}),
3054
+ selectedHostMcpDelegations: hostSelections,
3055
+ ...(captureSelectedHostAuthority || captureLinkedAuthority
3056
+ ? {
3057
+ captureInitialTurnAuthority: async (
3058
+ tx: Database,
3059
+ sessionId: string,
3060
+ turnId: string,
3061
+ ) => {
3062
+ await captureLinkedAuthority?.(tx, sessionId, turnId);
3063
+ await captureSelectedHostAuthority?.(tx, sessionId, turnId);
3064
+ },
3065
+ }
3066
+ : {}),
2979
3067
  ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
2980
3068
  ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
2981
3069
  createdByActor: creationInitiator.actor ?? null,
@@ -2996,7 +3084,7 @@ export async function createSessionForRequestWithOutcome(
2996
3084
  instructions: payload.instructions ?? null,
2997
3085
  policyRole: payload.policyRole ?? null,
2998
3086
  agentAccess: sessionScope.agentAccess,
2999
- endUser: sessionScope.endUser,
3087
+ scopeSubjectId: sessionScope.scopeSubjectId,
3000
3088
  memoryScope: sessionScope.memoryScope,
3001
3089
  firstPartyMcpPermissions,
3002
3090
  firstPartyMcpTools,
@@ -3057,6 +3145,18 @@ export async function createSessionForRequestWithOutcome(
3057
3145
  { message: error.message, cause: error },
3058
3146
  );
3059
3147
  }
3148
+ if (error instanceof HostMcpDelegationAuthorityError) {
3149
+ throw new HTTPException(403, {
3150
+ message: "Host delegation authority unavailable",
3151
+ cause: error,
3152
+ });
3153
+ }
3154
+ if (error instanceof HostMcpBindingConflictError) {
3155
+ throw new HTTPException(409, {
3156
+ message: "Host delegation selection conflicts",
3157
+ cause: error,
3158
+ });
3159
+ }
3060
3160
  if (error instanceof AgentCommandAuthorityError) {
3061
3161
  throw new HTTPException(403, { message: error.message });
3062
3162
  }
@@ -3111,6 +3211,94 @@ export async function createSessionForRequest(
3111
3211
  ).session;
3112
3212
  }
3113
3213
 
3214
+ /** Snapshot the selected destination, then recheck live actor, binding and grant
3215
+ * inside the transaction which accepts the turn. Neither selection nor a
3216
+ * registered binding alone is execution authority. Shared by create/send/steer. */
3217
+ function prepareSelectedHostTurnAuthority(
3218
+ settings: Settings,
3219
+ tools: ToolRef[],
3220
+ grant: AccessGrant,
3221
+ workspaceId: string,
3222
+ selections: HostMcpCreateSelection[],
3223
+ authorization?: AccessGrantAuthorization,
3224
+ ): ((tx: Database, sessionId: string, turnId: string) => Promise<void>) | undefined {
3225
+ if (!selections.length) return undefined;
3226
+ if (
3227
+ !authorization ||
3228
+ authorization.grant.accountId !== grant.accountId ||
3229
+ authorization.grant.subjectId !== grant.subjectId ||
3230
+ authorization?.grant.workspaceId !== workspaceId ||
3231
+ !hasPermission(authorization?.grant.permissions ?? [], "connections:read") ||
3232
+ !hasPermission(grant.permissions, "connections:read") ||
3233
+ grant.metadata?.["sessionId"]
3234
+ )
3235
+ throw new HTTPException(403, {
3236
+ message: "Host selection requires a verified direct owner",
3237
+ });
3238
+ const beforeCommit = prepareHostMcpOwnerAuthorization(
3239
+ authorization,
3240
+ workspaceId,
3241
+ "connections:read",
3242
+ );
3243
+ const configs = new Map(
3244
+ selections.map((selection) => {
3245
+ const configured = settings.mcpServers.find((server) => server.id === selection.serverId);
3246
+ if (
3247
+ !configured ||
3248
+ !tools.some((tool) => tool.kind === "mcp" && tool.id === selection.serverId) ||
3249
+ configured.connectionRef?.authoritySource !== "host" ||
3250
+ !configured.connectionRef.hostBinding ||
3251
+ !configured.url
3252
+ )
3253
+ throw new HTTPException(422, {
3254
+ message: "Host delegation must match a selected configured host server",
3255
+ });
3256
+ assertHostMcpAuthoritySourceAdmissionEnabled(settings, configured.connectionRef);
3257
+ return [selection.serverId, structuredClone(configured)] as const;
3258
+ }),
3259
+ );
3260
+ return async (tx, sessionId, turnId) => {
3261
+ const owner = await beforeCommit(tx);
3262
+ for (const selection of selections) {
3263
+ const delegation = await getHostMcpDelegation(tx, owner, selection.delegationId);
3264
+ const binding = delegation ? await getHostMcpBinding(tx, owner, delegation.bindingId) : null;
3265
+ const configured = configs.get(selection.serverId)!;
3266
+ const { hostBinding, ...connectionRef } = configured.connectionRef!;
3267
+ const definition = HostMcpBindingDefinition.safeParse({
3268
+ serverId: selection.serverId,
3269
+ destinationUrl: configured.url,
3270
+ connectionRef,
3271
+ });
3272
+ if (
3273
+ !delegation ||
3274
+ !binding ||
3275
+ delegation.status !== "active" ||
3276
+ binding.status !== "active" ||
3277
+ !definition.success ||
3278
+ delegation.generation !== selection.generation ||
3279
+ binding.id !== hostBinding!.bindingId ||
3280
+ binding.generation !== hostBinding!.generation ||
3281
+ stableJson(definition.data) !== stableJson(binding.definition)
3282
+ )
3283
+ throw new HTTPException(403, { message: "Host delegation selection changed" });
3284
+ try {
3285
+ await captureDirectHostMcpAuthority(tx, owner, {
3286
+ sessionId,
3287
+ turnId,
3288
+ delegationId: delegation.id,
3289
+ expectedDelegationGeneration: selection.generation,
3290
+ });
3291
+ } catch (error) {
3292
+ if (error instanceof HostMcpDelegationAuthorityError)
3293
+ throw new HTTPException(403, { message: "Host delegation authority unavailable" });
3294
+ if (error instanceof HostMcpBindingConflictError)
3295
+ throw new HTTPException(409, { message: "Host delegation selection conflicts" });
3296
+ throw error;
3297
+ }
3298
+ }
3299
+ };
3300
+ }
3301
+
3114
3302
  function sessionPromptBoundaryRequestHash(input: {
3115
3303
  delivery: "send" | "steer";
3116
3304
  controlEtag: string | null;
@@ -3126,6 +3314,7 @@ function sessionPromptBoundaryRequestHash(input: {
3126
3314
  source: "user" | "api";
3127
3315
  mcpCredentialUpdates: SessionMcpCredentialUpdateInput[];
3128
3316
  connectionAuthorities?: McpConnectionAuthoritySelection[];
3317
+ selectedHostMcpDelegations?: HostMcpCreateSelection[];
3129
3318
  personalResourceAttachment?: PersonalResourceAttachmentIntent;
3130
3319
  commandActor: SessionCommandActor;
3131
3320
  }): string {
@@ -3145,6 +3334,9 @@ function sessionPromptBoundaryRequestHash(input: {
3145
3334
  source: input.source,
3146
3335
  mcpCredentialUpdates: input.mcpCredentialUpdates,
3147
3336
  connectionAuthorities: input.connectionAuthorities ?? [],
3337
+ ...(input.selectedHostMcpDelegations?.length
3338
+ ? { selectedHostMcpDelegations: input.selectedHostMcpDelegations }
3339
+ : {}),
3148
3340
  personalResourceAttachment: input.personalResourceAttachment ?? null,
3149
3341
  ...(input.commandActor.type === "service"
3150
3342
  ? {
@@ -3183,6 +3375,7 @@ export async function acceptSessionUserMessageWithOutcome(
3183
3375
  clientEventId?: string;
3184
3376
  mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
3185
3377
  connectionAuthorities?: McpConnectionAuthoritySelection[];
3378
+ selectedHostMcpDelegations?: HostMcpCreateSelection[];
3186
3379
  delivery?: "send" | "steer";
3187
3380
  origin?: "human" | "operator";
3188
3381
  controlEtag?: string | null;
@@ -3200,6 +3393,24 @@ export async function acceptSessionUserMessageWithOutcome(
3200
3393
  replay: boolean;
3201
3394
  }> {
3202
3395
  const { db, bus, workflowClient, objectStorage } = deps;
3396
+ const hostSelections = HostMcpCreateSelections.parse(input.selectedHostMcpDelegations ?? []).sort(
3397
+ (a, b) => (a.serverId < b.serverId ? -1 : a.serverId > b.serverId ? 1 : 0),
3398
+ );
3399
+ if (
3400
+ hostSelections.length &&
3401
+ (!input.authorization ||
3402
+ input.authorization.grant.accountId !== grant.accountId ||
3403
+ input.authorization.grant.subjectId !== grant.subjectId ||
3404
+ input.authorization?.grant.workspaceId !== workspaceId ||
3405
+ !hasPermission(input.authorization?.grant.permissions ?? [], "connections:read") ||
3406
+ !hasPermission(grant.permissions, "connections:read") ||
3407
+ grant.metadata?.["sessionId"])
3408
+ )
3409
+ throw new HTTPException(403, {
3410
+ message: "Host selection requires a verified direct owner",
3411
+ });
3412
+ if (hostSelections.length)
3413
+ prepareHostMcpOwnerAuthorization(input.authorization!, workspaceId, "connections:read");
3203
3414
  const delegatedServiceInitiator = serviceInitiatorForGrant(grant);
3204
3415
  const delivery = input.delivery ?? "send";
3205
3416
  const source = delegatedServiceInitiator || input.origin === "operator" ? "api" : "user";
@@ -3237,6 +3448,7 @@ export async function acceptSessionUserMessageWithOutcome(
3237
3448
  latencyMode: input.latencyMode ?? null,
3238
3449
  source,
3239
3450
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
3451
+ selectedHostMcpDelegations: hostSelections,
3240
3452
  ...(input.connectionAuthorities
3241
3453
  ? { connectionAuthorities: input.connectionAuthorities }
3242
3454
  : {}),
@@ -3421,6 +3633,15 @@ export async function acceptSessionUserMessageWithOutcome(
3421
3633
  existingSession.firstPartyMcpPermissions.includes("connections:read")),
3422
3634
  ...(input.connectionAuthorities ? { authoritySelections: input.connectionAuthorities } : {}),
3423
3635
  });
3636
+ const captureSelectedHostAuthority = prepareSelectedHostTurnAuthority(
3637
+ runtimeSettings,
3638
+ existingSession.tools,
3639
+ grant,
3640
+ workspaceId,
3641
+ hostSelections,
3642
+ input.authorization,
3643
+ );
3644
+ const captureLinkedAuthority = prepareExternalLinkTurnAdmission(input.authorization);
3424
3645
  const { accepted, turn, draft, receipt, routing, interruptionCount, replay } =
3425
3646
  await postUserMessageTurn({
3426
3647
  db,
@@ -3442,6 +3663,15 @@ export async function acceptSessionUserMessageWithOutcome(
3442
3663
  turnExecutionPolicy,
3443
3664
  mcpCredentialUpdates,
3444
3665
  personalConnectionDelegations,
3666
+ selectedHostMcpDelegations: hostSelections,
3667
+ ...(captureSelectedHostAuthority || captureLinkedAuthority
3668
+ ? {
3669
+ captureTurnAuthority: async (tx: Database, turnId: string) => {
3670
+ await captureLinkedAuthority?.(tx, sessionId, turnId);
3671
+ await captureSelectedHostAuthority?.(tx, sessionId, turnId);
3672
+ },
3673
+ }
3674
+ : {}),
3445
3675
  ...(input.personalResourceAttachment
3446
3676
  ? { personalResourceAttachment: input.personalResourceAttachment }
3447
3677
  : {}),
package/src/index.ts CHANGED
@@ -61,6 +61,13 @@ export * from "./sandbox/runtime-settings";
61
61
 
62
62
  // Access layer (transport-neutral grant resolution + permission checks).
63
63
  export * from "./access";
64
+ export * from "./application/external-workspace-members";
65
+ export * from "./application/external-identity-lifecycle";
66
+ export * from "./application/external-continuation";
67
+ export * from "./application/external-link-work-admission";
68
+ export * from "./application/connect-authority";
69
+ export * from "./application/host-mcp-owner";
70
+ export * from "./application/connect-operation";
64
71
  export * from "./session-authorization";
65
72
 
66
73
  // Billing / usage-limit admission (checkLimit / requireLimit / recordWorkspaceUsage).