@opengeni/api-router 0.15.6 → 0.16.1

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.
package/dist/app.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  validateToolRefs,
23
23
  withDefaultEnabledCapabilityMcpTools,
24
24
  workflowIdForSession
25
- } from "./chunk-MN7BW3UV.js";
25
+ } from "./chunk-ZD5YBUNA.js";
26
26
  export {
27
27
  API_MAX_REQUEST_BODY_BYTES,
28
28
  allowedCorsOrigin,
@@ -627,6 +627,12 @@ async function startMcpOAuth(deps, context) {
627
627
  const providerDomain = officialSlackResource ? "slack.com" : canonicalProviderDomain(context.payload.providerDomain ?? new URL(mcpUrl).hostname);
628
628
  const personalSlack = officialSlackResource || providerDomain === "slack.com";
629
629
  assertPersonalSlackOAuthStart(settings, context.payload, mcpUrl, personalSlack);
630
+ if (personalSlack && context.payload.ownership === "workspace") {
631
+ throw new HTTPException2(422, {
632
+ message: "Slack's hosted MCP connection is personal; use the OpenGeni Slack bot installation for workspace access"
633
+ });
634
+ }
635
+ const requestedOwnership = personalSlack ? "personal" : context.payload.ownership ?? "workspace";
630
636
  const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
631
637
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
632
638
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
@@ -637,11 +643,14 @@ async function startMcpOAuth(deps, context) {
637
643
  providerDomain,
638
644
  mcpUrl,
639
645
  personalSlack,
640
- connectionId: context.payload.connectionId
646
+ connectionId: context.payload.connectionId,
647
+ requestedOwnership: context.payload.ownership,
648
+ newConnectionOwnership: requestedOwnership
641
649
  });
642
650
  if (context.payload.connectionId && !existing) {
643
651
  throw new HTTPException2(404, { message: "connection not found" });
644
652
  }
653
+ const ownership = existing ? ownershipForConnection(existing.subjectId, context.subjectId) : requestedOwnership;
645
654
  const discovery = await discoverMcpOAuth(mcpUrl, settings);
646
655
  if (personalSlack && !isLocalTestEnvironment(settings.environment)) {
647
656
  assertSlackAuthorizationServer(discovery.as);
@@ -667,6 +676,7 @@ async function startMcpOAuth(deps, context) {
667
676
  accountId: context.accountId,
668
677
  workspaceId: context.workspaceId,
669
678
  subjectId: context.subjectId,
679
+ ownership,
670
680
  providerDomain,
671
681
  mcpUrl,
672
682
  resource,
@@ -739,6 +749,7 @@ async function completeMcpOAuthCallback(deps, input) {
739
749
  })
740
750
  };
741
751
  }
752
+ const ownerSubjectId = state.ownership === "personal" ? state.subjectId : null;
742
753
  try {
743
754
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, input.requestUrl);
744
755
  const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
@@ -781,7 +792,7 @@ async function completeMcpOAuthCallback(deps, input) {
781
792
  connectionId: state.connectionId,
782
793
  visibleToSubjectId: state.subjectId,
783
794
  expectedVersion: state.connectionVersion,
784
- subjectId: state.subjectId,
795
+ subjectId: ownerSubjectId,
785
796
  providerDomain: state.providerDomain,
786
797
  kind: "oauth2",
787
798
  status: "active",
@@ -793,7 +804,7 @@ async function completeMcpOAuthCallback(deps, input) {
793
804
  }) : createConnection(db, {
794
805
  accountId: state.accountId,
795
806
  workspaceId: state.workspaceId,
796
- subjectId: state.subjectId,
807
+ subjectId: ownerSubjectId,
797
808
  providerDomain: state.providerDomain,
798
809
  kind: "oauth2",
799
810
  credentialEncrypted,
@@ -812,6 +823,7 @@ async function completeMcpOAuthCallback(deps, input) {
812
823
  redirectTo: callbackReturnPath(state.returnPath, "success", {
813
824
  connectionId: connection.id,
814
825
  providerDomain: connection.providerDomain,
826
+ ownership: state.ownership,
815
827
  ...verification.metadata.status === "failed" ? { verification: "failed" } : {}
816
828
  })
817
829
  };
@@ -1213,17 +1225,32 @@ async function existingOAuthConnectionForStart(db, input) {
1213
1225
  input.connectionId,
1214
1226
  input.subjectId
1215
1227
  );
1216
- return connection?.subjectId === input.subjectId && connection.kind === "oauth2" && connection.providerDomain === input.providerDomain && (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl) ? connection : null;
1228
+ if (!connection || connection.kind !== "oauth2") {
1229
+ return null;
1230
+ }
1231
+ const ownership = ownershipForConnection(connection.subjectId, input.subjectId);
1232
+ if (input.requestedOwnership && input.requestedOwnership !== ownership) {
1233
+ throw new HTTPException2(409, {
1234
+ message: "connection ownership cannot be changed during OAuth reconnect"
1235
+ });
1236
+ }
1237
+ return connection.providerDomain === input.providerDomain && (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl) ? connection : null;
1217
1238
  }
1218
1239
  const visible = await listConnectionsMetadata(db, input.workspaceId, input.subjectId);
1240
+ const ownerSubjectId = input.newConnectionOwnership === "personal" ? input.subjectId : null;
1219
1241
  const matching = visible.filter(
1220
- (connection) => connection.subjectId === input.subjectId && connection.kind === "oauth2" && connection.providerDomain === input.providerDomain && (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl)
1242
+ (connection) => connection.subjectId === ownerSubjectId && connection.kind === "oauth2" && connection.providerDomain === input.providerDomain && (!input.personalSlack || connection.metadata.mcpUrl === input.mcpUrl)
1221
1243
  );
1222
1244
  if (input.personalSlack) {
1223
1245
  return selectCanonicalPersonalSlackConnection(matching);
1224
1246
  }
1225
1247
  return matching.find((connection) => connection.status === "active") ?? null;
1226
1248
  }
1249
+ function ownershipForConnection(subjectId, authenticatingSubjectId) {
1250
+ if (subjectId === null) return "workspace";
1251
+ if (subjectId === authenticatingSubjectId) return "personal";
1252
+ throw new HTTPException2(404, { message: "connection not found" });
1253
+ }
1227
1254
  function buildAuthorizationUrl(input) {
1228
1255
  const endpoint = oauthEndpointUrl(input.endpoint, input.settings, "OAuth authorization endpoint");
1229
1256
  const url = new URL(endpoint);
@@ -1254,6 +1281,9 @@ function readOAuthState(state, settings) {
1254
1281
  accountId: requiredString(payload.accountId, "state.accountId"),
1255
1282
  workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
1256
1283
  subjectId: requiredString(payload.subjectId, "state.subjectId"),
1284
+ // OAuth states minted before ownership was explicit were always personal.
1285
+ // Preserve that meaning for in-flight reconnects during a rolling deploy.
1286
+ ownership: connectionOwnership(payload.ownership) ?? "personal",
1257
1287
  providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
1258
1288
  mcpUrl: stringValue(payload.mcpUrl) ?? resource,
1259
1289
  resource,
@@ -1297,6 +1327,9 @@ function readOAuthState(state, settings) {
1297
1327
  ...connectionVersion !== void 0 ? { connectionVersion } : {}
1298
1328
  };
1299
1329
  }
1330
+ function connectionOwnership(value) {
1331
+ return value === "workspace" || value === "personal" ? value : void 0;
1332
+ }
1300
1333
  async function clientForState(db, settings, state) {
1301
1334
  if (state.clientRegistrationMethod === "cimd") {
1302
1335
  return {
@@ -2669,6 +2702,7 @@ import {
2669
2702
  requireVariableSetEncryption
2670
2703
  } from "@opengeni/core";
2671
2704
  import {
2705
+ captureScheduledTaskRestoreState,
2672
2706
  createValidatedScheduledTask,
2673
2707
  manualScheduledTaskTriggerUsageKey,
2674
2708
  manualScheduledTaskTriggerWorkflowId,
@@ -5998,6 +6032,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
5998
6032
  },
5999
6033
  async ({ id, ...raw }) => {
6000
6034
  const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
6035
+ const previous = await captureScheduledTaskRestoreState(deps.db, existing);
6001
6036
  const payload = UpdateScheduledTaskRequest.parse(raw);
6002
6037
  requireVariableSetsUseForMcpAttachment(grant, payload.variableSetId);
6003
6038
  const update = await validatedScheduledTaskUpdate({
@@ -6013,7 +6048,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
6013
6048
  await syncUpdatedScheduledTask({
6014
6049
  db: deps.db,
6015
6050
  workflowClient: deps.workflowClient,
6016
- previous: existing,
6051
+ previous,
6017
6052
  task
6018
6053
  });
6019
6054
  return json(task);
@@ -6027,13 +6062,14 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
6027
6062
  },
6028
6063
  async ({ id }) => {
6029
6064
  const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
6065
+ const previous = await captureScheduledTaskRestoreState(deps.db, existing);
6030
6066
  const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
6031
6067
  status: "paused"
6032
6068
  });
6033
6069
  await syncUpdatedScheduledTask({
6034
6070
  db: deps.db,
6035
6071
  workflowClient: deps.workflowClient,
6036
- previous: existing,
6072
+ previous,
6037
6073
  task
6038
6074
  });
6039
6075
  return json(task);
@@ -6047,13 +6083,14 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
6047
6083
  },
6048
6084
  async ({ id }) => {
6049
6085
  const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
6086
+ const previous = await captureScheduledTaskRestoreState(deps.db, existing);
6050
6087
  const task = await updateScheduledTask(deps.db, grant.workspaceId, id, {
6051
6088
  status: "active"
6052
6089
  });
6053
6090
  await syncUpdatedScheduledTask({
6054
6091
  db: deps.db,
6055
6092
  workflowClient: deps.workflowClient,
6056
- previous: existing,
6093
+ previous,
6057
6094
  task
6058
6095
  });
6059
6096
  return json(task);
@@ -8136,6 +8173,7 @@ import {
8136
8173
  } from "@opengeni/contracts";
8137
8174
  import {
8138
8175
  hasPermission as hasPermission5,
8176
+ withFrozenPersonalConnectionDelegations,
8139
8177
  settingsWithEnabledCapabilityMcpServers
8140
8178
  } from "@opengeni/core";
8141
8179
  import {
@@ -8144,6 +8182,7 @@ import {
8144
8182
  clearPendingSessionToolspaceCall,
8145
8183
  getActiveSessionTurnForExecution,
8146
8184
  getSessionRootId,
8185
+ getWorkspaceGrant as getWorkspaceGrant3,
8147
8186
  listSessionMcpServerMetadata,
8148
8187
  listSessionMcpServersForRun,
8149
8188
  registerPendingSessionToolCall,
@@ -8266,6 +8305,7 @@ async function prepareToolspaceMcpSurface(input) {
8266
8305
  executionGeneration: activeTurn.executionGeneration
8267
8306
  };
8268
8307
  const session = await requireSession2(deps.db, grant.workspaceId, sessionId);
8308
+ const personalConnectionDelegations = activeTurn.personalConnectionDelegations;
8269
8309
  let rootSessionId = sessionId;
8270
8310
  if (deps.connectionCredentials?.mcpCredentials) {
8271
8311
  const resolvedRootSessionId = await getSessionRootId(deps.db, grant.workspaceId, sessionId);
@@ -8300,6 +8340,7 @@ async function prepareToolspaceMcpSurface(input) {
8300
8340
  rootSessionId,
8301
8341
  proxyableIds,
8302
8342
  activeTurn,
8343
+ personalConnectionDelegations,
8303
8344
  getRegistry: () => getRegistry(attemptAuthority.attemptId)
8304
8345
  });
8305
8346
  const tools = listing.map(
@@ -8309,6 +8350,7 @@ async function prepareToolspaceMcpSurface(input) {
8309
8350
  authority: attemptAuthority,
8310
8351
  rootSessionId,
8311
8352
  entry,
8353
+ personalConnectionDelegations,
8312
8354
  getRegistry
8313
8355
  })
8314
8356
  );
@@ -8342,7 +8384,16 @@ async function buildToolspaceRegistry(deps, workspaceId, sessionId, attemptId) {
8342
8384
  return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
8343
8385
  }
8344
8386
  async function resolveToolListing(input) {
8345
- const { deps, grant, sessionId, rootSessionId, proxyableIds, activeTurn, getRegistry } = input;
8387
+ const {
8388
+ deps,
8389
+ grant,
8390
+ sessionId,
8391
+ rootSessionId,
8392
+ proxyableIds,
8393
+ activeTurn,
8394
+ personalConnectionDelegations,
8395
+ getRegistry
8396
+ } = input;
8346
8397
  const cacheKey = await toolListCacheKey(
8347
8398
  deps,
8348
8399
  grant.workspaceId,
@@ -8373,7 +8424,8 @@ async function resolveToolListing(input) {
8373
8424
  config,
8374
8425
  sessionId,
8375
8426
  rootSessionId,
8376
- turn: activeTurn
8427
+ turn: activeTurn,
8428
+ personalConnectionDelegations
8377
8429
  }).catch((error) => {
8378
8430
  deps.observability?.warn("toolspace upstream connection failed", {
8379
8431
  serverId,
@@ -8510,7 +8562,15 @@ async function connectToolspaceServer(input) {
8510
8562
  };
8511
8563
  }
8512
8564
  function toolspaceToolFor(input) {
8513
- const { deps, grant, authority, rootSessionId, entry, getRegistry } = input;
8565
+ const {
8566
+ deps,
8567
+ grant,
8568
+ authority,
8569
+ rootSessionId,
8570
+ entry,
8571
+ personalConnectionDelegations,
8572
+ getRegistry
8573
+ } = input;
8514
8574
  const { sessionId } = authority;
8515
8575
  const { serverId, tool } = entry;
8516
8576
  const name = prefixedMcpToolName(serverId, tool.name);
@@ -8545,7 +8605,8 @@ function toolspaceToolFor(input) {
8545
8605
  config,
8546
8606
  sessionId,
8547
8607
  rootSessionId,
8548
- turn: reservation.turn
8608
+ turn: reservation.turn,
8609
+ personalConnectionDelegations
8549
8610
  }).catch(() => null);
8550
8611
  if (!connection) {
8551
8612
  return mcpError(`upstream tool failed: ${name}`);
@@ -8763,15 +8824,8 @@ function connectionBrokerFetch(baseFetch, input) {
8763
8824
  if (!connectionRef) {
8764
8825
  return baseFetch;
8765
8826
  }
8766
- const credentialSubjectId = input.turn.initiator.kind === "subject" ? input.turn.initiator.subjectId : void 0;
8767
- if (connectionRef.subjectScope === "subject" && !credentialSubjectId) {
8768
- throw new Error(
8769
- `subject-owned connection for MCP server ${input.config.id} requires a human turn initiator`
8770
- );
8771
- }
8772
8827
  const hostCredentialPort = input.deps.connectionCredentials?.mcpCredentials;
8773
- const resolverSubjectId = hostCredentialPort ? input.grant.subjectId : credentialSubjectId;
8774
- const resolveCredential = hostCredentialPort ? buildHostConnectionTokenResolver(hostCredentialPort, {
8828
+ const rawResolveCredential = hostCredentialPort ? buildHostConnectionTokenResolver(hostCredentialPort, {
8775
8829
  accountId: input.grant.accountId,
8776
8830
  workspaceId: input.grant.workspaceId,
8777
8831
  sessionId: input.sessionId,
@@ -8783,18 +8837,40 @@ function connectionBrokerFetch(baseFetch, input) {
8783
8837
  initiatorContext: input.turn.initiatorContext,
8784
8838
  surface: "toolspace"
8785
8839
  }) : buildConnectionTokenResolver2(input.deps.db, input.deps.settings);
8840
+ const personalDelegations = input.personalConnectionDelegations ?? [];
8841
+ const delegatedMembershipChecks = /* @__PURE__ */ new Map();
8842
+ const delegatedOwnerHasMembership = async (subjectId) => {
8843
+ const existing = delegatedMembershipChecks.get(subjectId);
8844
+ if (existing) return await existing;
8845
+ const check = getWorkspaceGrant3(input.deps.db, subjectId, input.grant.workspaceId).then(
8846
+ Boolean
8847
+ );
8848
+ delegatedMembershipChecks.set(subjectId, check);
8849
+ return await check;
8850
+ };
8851
+ const resolveCredential = withFrozenPersonalConnectionDelegations({
8852
+ resolveCredential: rawResolveCredential,
8853
+ settings: { mcpServers: [input.config] },
8854
+ personalConnectionDelegations: personalDelegations,
8855
+ ownerHasWorkspaceMembership: delegatedOwnerHasMembership
8856
+ });
8786
8857
  return async (requestInput, init) => {
8787
8858
  const request = await mcpRequestInfo(requestInput, init);
8788
8859
  const destinationUrl = mcpRequestDestinationUrl(requestInput);
8789
- const first = await resolveCredential({
8790
- workspaceId: input.grant.workspaceId,
8791
- serverId: input.config.id,
8792
- connectionRef,
8793
- destinationUrl,
8794
- forceRefresh: false,
8795
- ...request.toolName ? { toolName: request.toolName } : {},
8796
- ...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
8797
- });
8860
+ const resolverSubjectId = connectionRef.subjectScope !== "subject" && hostCredentialPort ? input.grant.subjectId : void 0;
8861
+ const resolve = async (forceRefresh) => {
8862
+ const result = await resolveCredential({
8863
+ workspaceId: input.grant.workspaceId,
8864
+ serverId: input.config.id,
8865
+ connectionRef,
8866
+ destinationUrl,
8867
+ forceRefresh,
8868
+ ...request.toolName ? { toolName: request.toolName } : {},
8869
+ ...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
8870
+ });
8871
+ return result;
8872
+ };
8873
+ const first = await resolve(false);
8798
8874
  if (first.status === "auth_needed") {
8799
8875
  return await authNeededFetchResponse(input, request, first);
8800
8876
  }
@@ -8804,15 +8880,7 @@ function connectionBrokerFetch(baseFetch, input) {
8804
8880
  );
8805
8881
  if (response.status === 401) {
8806
8882
  await cancelMcpResponseBody(response);
8807
- const refreshed = await resolveCredential({
8808
- workspaceId: input.grant.workspaceId,
8809
- serverId: input.config.id,
8810
- connectionRef,
8811
- destinationUrl,
8812
- forceRefresh: true,
8813
- ...request.toolName ? { toolName: request.toolName } : {},
8814
- ...resolverSubjectId ? { subjectId: resolverSubjectId } : {}
8815
- });
8883
+ const refreshed = await resolve(true);
8816
8884
  if (refreshed.status === "auth_needed") {
8817
8885
  return await authNeededFetchResponse(input, request, refreshed);
8818
8886
  }
@@ -8856,7 +8924,7 @@ function authNeededFromStatus(config, first, reason) {
8856
8924
  reason,
8857
8925
  providerDomain: connectionRef.providerDomain,
8858
8926
  ...connectionRef.provider ? { provider: connectionRef.provider } : {},
8859
- connectionId: first.connectionId,
8927
+ ...connectionRef.subjectScope === "subject" ? {} : { connectionId: first.connectionId },
8860
8928
  ...connectionRef.scopes ? { scopes: connectionRef.scopes } : {},
8861
8929
  ...connectionRef.resource ? { resource: connectionRef.resource } : {},
8862
8930
  ...connectionRef.selectedResources ? { selectedResources: connectionRef.selectedResources } : {}
@@ -10552,7 +10620,7 @@ import {
10552
10620
  createConnectionWithSlackBotSuccessAudit,
10553
10621
  encryptEnvironmentValue as encryptEnvironmentValue5,
10554
10622
  getConnectionMetadata as getConnectionMetadata3,
10555
- getWorkspaceGrant as getWorkspaceGrant4,
10623
+ getWorkspaceGrant as getWorkspaceGrant5,
10556
10624
  listConnectionsMetadata as listConnectionsMetadata3,
10557
10625
  recordSlackBotInstallCallbackFailure,
10558
10626
  revokeConnection,
@@ -10585,7 +10653,7 @@ import {
10585
10653
  decryptEnvironmentValue as decryptEnvironmentValue3,
10586
10654
  encryptEnvironmentValue as encryptEnvironmentValue4,
10587
10655
  getConnectionMetadata as getConnectionMetadata2,
10588
- getWorkspaceGrant as getWorkspaceGrant3,
10656
+ getWorkspaceGrant as getWorkspaceGrant4,
10589
10657
  loadConnectionCredentialForBroker,
10590
10658
  updateConnection as updateConnection2
10591
10659
  } from "@opengeni/db";
@@ -11026,7 +11094,7 @@ function readGoogleDriveOAuthState(raw, settings) {
11026
11094
  };
11027
11095
  }
11028
11096
  async function requireGoogleDriveCallbackGrant(deps, state) {
11029
- const grant = await getWorkspaceGrant3(deps.db, state.subjectId, state.workspaceId);
11097
+ const grant = await getWorkspaceGrant4(deps.db, state.subjectId, state.workspaceId);
11030
11098
  if (!grant || grant.accountId !== state.accountId || !hasPermission7(grant.permissions, "connections:write")) {
11031
11099
  throw new HTTPException10(403, {
11032
11100
  message: "Google Drive OAuth subject no longer has permission for this workspace"
@@ -11269,7 +11337,7 @@ function registerConnectionRoutes(app, deps) {
11269
11337
  const payload = CreateConnectionRequest.parse(await c.req.json());
11270
11338
  assertNotReservedSlackBotMetadata(payload.metadata);
11271
11339
  const key = requireEnvironmentEncryption4(settings);
11272
- const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
11340
+ const subjectId = createConnectionSubjectId(payload, grant.subjectId);
11273
11341
  const providerDomain = canonicalProviderDomain(payload.providerDomain);
11274
11342
  assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
11275
11343
  assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
@@ -11838,7 +11906,7 @@ function slackInstallErrorReason(error) {
11838
11906
  return "installation_failed";
11839
11907
  }
11840
11908
  async function requireSlackInstallCallbackGrant(db, state) {
11841
- const grant = await getWorkspaceGrant4(db, state.subjectId, state.workspaceId);
11909
+ const grant = await getWorkspaceGrant5(db, state.subjectId, state.workspaceId);
11842
11910
  if (!grant || grant.accountId !== state.accountId || !hasPermission8(grant.permissions, "connections:write")) {
11843
11911
  throw new SlackInstallCallbackError(
11844
11912
  403,
@@ -11877,6 +11945,18 @@ function writableSubjectId(requested, grantSubjectId) {
11877
11945
  }
11878
11946
  return requested;
11879
11947
  }
11948
+ function createConnectionSubjectId(payload, grantSubjectId) {
11949
+ if (payload.ownership === void 0) {
11950
+ return writableSubjectId(payload.subjectId, grantSubjectId);
11951
+ }
11952
+ const subjectId = payload.ownership === "personal" ? grantSubjectId : null;
11953
+ if (payload.subjectId !== void 0 && payload.subjectId !== subjectId) {
11954
+ throw new HTTPException11(422, {
11955
+ message: "ownership and subjectId describe different connection owners"
11956
+ });
11957
+ }
11958
+ return subjectId;
11959
+ }
11880
11960
  function encryptCredentialBundle(key, credential) {
11881
11961
  return encryptEnvironmentValue5(key, JSON.stringify(credential));
11882
11962
  }
@@ -15824,6 +15904,7 @@ import {
15824
15904
  import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
15825
15905
  import { recordWorkspaceUsage as recordWorkspaceUsage4, requireLimit as requireLimit6 } from "@opengeni/core";
15826
15906
  import {
15907
+ captureScheduledTaskRestoreState as captureScheduledTaskRestoreState2,
15827
15908
  createValidatedScheduledTask as createValidatedScheduledTask3,
15828
15909
  manualScheduledTaskTriggerUsageKey as manualScheduledTaskTriggerUsageKey2,
15829
15910
  manualScheduledTaskTriggerWorkflowId as manualScheduledTaskTriggerWorkflowId2,
@@ -15873,6 +15954,7 @@ function registerScheduledTaskRoutes(app, deps) {
15873
15954
  const grant = await requireAccessGrant13(c, deps, workspaceId, "scheduled_tasks:manage");
15874
15955
  const taskId = c.req.param("taskId");
15875
15956
  const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
15957
+ const previous = await captureScheduledTaskRestoreState2(db, existing);
15876
15958
  const rawPayload = await c.req.json();
15877
15959
  const payload = UpdateScheduledTaskRequest2.parse(rawPayload);
15878
15960
  const update = await validatedScheduledTaskUpdate2({
@@ -15885,23 +15967,25 @@ function registerScheduledTaskRoutes(app, deps) {
15885
15967
  toolsProvided: scheduledTaskToolsProvided2(rawPayload)
15886
15968
  });
15887
15969
  const task = await updateScheduledTask2(db, workspaceId, taskId, update);
15888
- await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
15970
+ await syncUpdatedScheduledTask2({ db, workflowClient, previous, task });
15889
15971
  return c.json(task);
15890
15972
  });
15891
15973
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/pause", async (c) => {
15892
15974
  const workspaceId = c.req.param("workspaceId");
15893
15975
  await requireAccessGrant13(c, deps, workspaceId, "scheduled_tasks:manage");
15894
15976
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
15977
+ const previous = await captureScheduledTaskRestoreState2(db, existing);
15895
15978
  const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "paused" });
15896
- await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
15979
+ await syncUpdatedScheduledTask2({ db, workflowClient, previous, task });
15897
15980
  return c.json(task);
15898
15981
  });
15899
15982
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
15900
15983
  const workspaceId = c.req.param("workspaceId");
15901
15984
  await requireAccessGrant13(c, deps, workspaceId, "scheduled_tasks:manage");
15902
15985
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
15986
+ const previous = await captureScheduledTaskRestoreState2(db, existing);
15903
15987
  const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "active" });
15904
- await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
15988
+ await syncUpdatedScheduledTask2({ db, workflowClient, previous, task });
15905
15989
  return c.json(task);
15906
15990
  });
15907
15991
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/trigger", async (c) => {
@@ -21560,9 +21644,10 @@ import {
21560
21644
  deleteSlackBotUserLink,
21561
21645
  enqueueSlackInteractionInbox,
21562
21646
  getOrCreateSlackInteraction,
21647
+ getLatestSessionModelForSubject,
21563
21648
  getSlackBotUserLink,
21564
21649
  getSlackInteractionByRoute,
21565
- getWorkspaceGrant as getWorkspaceGrant5,
21650
+ getWorkspaceGrant as getWorkspaceGrant6,
21566
21651
  listSessionEventPage as listSessionEventPage3,
21567
21652
  listSessionHumanInputRequests as listSessionHumanInputRequests2,
21568
21653
  rekeySlackInteractionRoute,
@@ -21825,6 +21910,14 @@ async function drainSlackInteractionsOnce(deps) {
21825
21910
  });
21826
21911
  } catch (error) {
21827
21912
  const code = safeErrorCode(error);
21913
+ console.error("[slack-interactions] inbox processing failed", {
21914
+ workspaceId: entry.workspaceId,
21915
+ connectionId: entry.connectionId,
21916
+ providerEventId: entry.providerEventId,
21917
+ triggerKind: entry.triggerKind,
21918
+ attemptCount: entry.attemptCount,
21919
+ errorCode: code
21920
+ });
21828
21921
  if (entry.attemptCount >= 5 || permanentSlackInteractionError(error) || permanentSlackDeliveryError(error)) {
21829
21922
  await settleSlackInteractionInbox(deps.db, {
21830
21923
  entry,
@@ -21929,7 +22022,7 @@ async function processSlackInboxEntry(deps, entry) {
21929
22022
  });
21930
22023
  return;
21931
22024
  }
21932
- const grant = await getWorkspaceGrant5(deps.db, link.subjectId, entry.workspaceId, {
22025
+ const grant = await getWorkspaceGrant6(deps.db, link.subjectId, entry.workspaceId, {
21933
22026
  principalKind: "human_session"
21934
22027
  });
21935
22028
  if (!grant || grant.accountId !== entry.accountId) {
@@ -21958,14 +22051,33 @@ async function processSlackInboxEntry(deps, entry) {
21958
22051
  await continueSlackSession(deps, grant, interaction, entry);
21959
22052
  return;
21960
22053
  }
21961
- const session = await createSessionForRequest3(deps, grant, entry.workspaceId, {
21962
- requestedSessionId: interaction.sessionReservationId,
21963
- initialMessage: entry.text,
21964
- turnInstructions: SLACK_TASK_INSTRUCTIONS,
21965
- firstPartyMcpTools: [...SLACK_TASK_FIRST_PARTY_MCP_TOOLS],
21966
- idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
21967
- clientEventId: `slack:${entry.providerEventId}`
21968
- });
22054
+ const preferredModel = await getLatestSessionModelForSubject(
22055
+ deps.db,
22056
+ entry.workspaceId,
22057
+ grant.subjectId
22058
+ );
22059
+ let session;
22060
+ try {
22061
+ session = await createSessionForRequest3(deps, grant, entry.workspaceId, {
22062
+ requestedSessionId: interaction.sessionReservationId,
22063
+ initialMessage: entry.text,
22064
+ turnInstructions: SLACK_TASK_INSTRUCTIONS,
22065
+ firstPartyMcpTools: [...SLACK_TASK_FIRST_PARTY_MCP_TOOLS],
22066
+ ...preferredModel ? { model: preferredModel } : {},
22067
+ idempotencyKey: `slack:${entry.connectionId}:${entry.providerEventId}`,
22068
+ clientEventId: `slack:${entry.providerEventId}`
22069
+ });
22070
+ } catch (error) {
22071
+ if (error instanceof HTTPException32) {
22072
+ await client.postMessage({
22073
+ operationId: deterministicUuid(`slack-admission-failed:${interaction.id}`),
22074
+ channelId: entry.slackChannelId,
22075
+ ...entry.triggerKind === "slash_command" ? {} : { threadTimestamp: entry.slackThreadTs ?? entry.slackMessageTs },
22076
+ text: slackAdmissionFailureText(error)
22077
+ });
22078
+ }
22079
+ throw error;
22080
+ }
21969
22081
  const bound = await bindSlackInteractionSession(deps.db, {
21970
22082
  ...interaction,
21971
22083
  owningSubjectId: grant.subjectId,
@@ -22356,9 +22468,19 @@ function safePayloadText(payload, field) {
22356
22468
  }
22357
22469
  function safeErrorCode(error) {
22358
22470
  if (error instanceof SlackBotProviderError) return error.code.slice(0, 128);
22471
+ if (error instanceof HTTPException32) return `http_${error.status}`;
22359
22472
  const raw = error instanceof Error ? error.name : "slack_interaction_error";
22360
22473
  return raw.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, 128) || "error";
22361
22474
  }
22475
+ function slackAdmissionFailureText(error) {
22476
+ if (error.status === 402) {
22477
+ return "OpenGeni could not start this task because the selected model has no available billing source. Open OpenGeni, select a connected subscription model, and try again.";
22478
+ }
22479
+ if (error.status === 429) {
22480
+ return "OpenGeni could not start this task because this workspace has reached a usage limit. Try again later or review the workspace limits in OpenGeni.";
22481
+ }
22482
+ return "OpenGeni could not start this task because the workspace rejected the session settings. Open OpenGeni, select an available model, and try again.";
22483
+ }
22362
22484
  var SlackInteractionPermanentError = class extends Error {
22363
22485
  };
22364
22486
  function permanentSlackInteractionError(error) {
@@ -23418,4 +23540,4 @@ export {
23418
23540
  withDefaultEnabledCapabilityMcpTools,
23419
23541
  workflowIdForSession2 as workflowIdForSession
23420
23542
  };
23421
- //# sourceMappingURL=chunk-MN7BW3UV.js.map
23543
+ //# sourceMappingURL=chunk-ZD5YBUNA.js.map