@opengeni/api-router 2.5.0 → 2.6.4

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 (59) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  3. package/dist/auth/managed-auth.d.ts +24 -1
  4. package/dist/{chunk-QESX7HDK.js → chunk-XTLI3CBH.js} +2236 -387
  5. package/dist/chunk-XTLI3CBH.js.map +1 -0
  6. package/dist/http/sse.d.ts +9 -0
  7. package/dist/index.js +104 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/interaction-metrics.d.ts +2 -0
  10. package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
  11. package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
  12. package/dist/mcp/remember.d.ts +2 -2
  13. package/dist/mcp/server.d.ts +1 -0
  14. package/dist/mcp/session-view.d.ts +1 -0
  15. package/dist/mcp/session-wait.d.ts +19 -0
  16. package/dist/routes/api-keys.d.ts +2 -0
  17. package/dist/routes/browser-sessions.d.ts +1 -0
  18. package/dist/routes/computer-sessions.d.ts +12 -0
  19. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  20. package/dist/routes/workspaces.d.ts +1 -1
  21. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  22. package/dist/workspace-delete-observability.d.ts +10 -0
  23. package/package.json +15 -15
  24. package/src/app.ts +172 -20
  25. package/src/auth/managed-auth-attempt-context.ts +40 -3
  26. package/src/auth/managed-auth-session-adapter.ts +1 -0
  27. package/src/auth/managed-auth.ts +164 -4
  28. package/src/http/sse.ts +279 -45
  29. package/src/integrations/oauth-client.ts +8 -1
  30. package/src/integrations/provider-oauth.ts +12 -2
  31. package/src/interaction-metrics.ts +30 -0
  32. package/src/mcp/company-brain-governed-writes.ts +38 -23
  33. package/src/mcp/company-profile-agent-admin.ts +7 -7
  34. package/src/mcp/remember.ts +19 -8
  35. package/src/mcp/server.ts +318 -26
  36. package/src/mcp/session-wait.ts +56 -8
  37. package/src/routes/api-integrations.ts +2 -2
  38. package/src/routes/api-keys.ts +149 -7
  39. package/src/routes/browser-sessions.ts +10 -2
  40. package/src/routes/capabilities.ts +3 -3
  41. package/src/routes/codex.ts +483 -74
  42. package/src/routes/company-profile.ts +64 -0
  43. package/src/routes/computer-sessions.ts +103 -1
  44. package/src/routes/integration-facets.ts +8 -5
  45. package/src/routes/interaction-resources.ts +7 -1
  46. package/src/routes/managed-auth-session-sets.ts +199 -2
  47. package/src/routes/organization-memberships.ts +28 -8
  48. package/src/routes/packs.ts +5 -5
  49. package/src/routes/plugins.ts +2 -2
  50. package/src/routes/scheduled-tasks.ts +9 -0
  51. package/src/routes/sessions.ts +3 -6
  52. package/src/routes/skills.ts +3 -3
  53. package/src/routes/workspaces.ts +293 -54
  54. package/src/sandbox/channel-a.ts +10 -4
  55. package/src/sandbox/machines.ts +13 -6
  56. package/src/sandbox/metrics-ingestion.ts +157 -3
  57. package/src/sandbox/viewer.ts +20 -1
  58. package/src/workspace-delete-observability.ts +75 -0
  59. package/dist/chunk-QESX7HDK.js.map +0 -1
@@ -42,6 +42,7 @@ import {
42
42
  withSessionRlsActorContext
43
43
  } from "@opengeni/db";
44
44
  import { requireSessionEventDurableFanoutCapability as requireSessionEventDurableFanoutCapability2 } from "@opengeni/events";
45
+ import { githubAppBotIdentityWarnings } from "@opengeni/github";
45
46
  import { createObservability } from "@opengeni/observability";
46
47
  import { createObjectStorage } from "@opengeni/storage";
47
48
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport3 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -122,6 +123,7 @@ import {
122
123
  } from "@opengeni/core";
123
124
 
124
125
  // src/auth/managed-auth.ts
126
+ import { canonicalPublicOrigin } from "@opengeni/config";
125
127
  import { ensureManagedAccessForUser } from "@opengeni/db";
126
128
  import {
127
129
  ensureCanonicalHumanIdentityForAuthUser,
@@ -1298,8 +1300,14 @@ function denied(intent, code) {
1298
1300
  // src/auth/managed-auth-attempt-context.ts
1299
1301
  import { AsyncLocalStorage } from "async_hooks";
1300
1302
  var managedAuthAttemptStorage = new AsyncLocalStorage();
1301
- function runManagedAuthAttempt(transactionId, action) {
1302
- return managedAuthAttemptStorage.run({ kind: "isolated_transaction", transactionId }, action);
1303
+ function runManagedAuthAttempt(transactionId, providerId, action) {
1304
+ return managedAuthAttemptStorage.run(
1305
+ { kind: "isolated_transaction", transactionId, providerId, createdAuthSessionId: null },
1306
+ action
1307
+ );
1308
+ }
1309
+ function runManagedAuthProvider(providerId, action) {
1310
+ return managedAuthAttemptStorage.run({ kind: "provider", providerId }, action);
1303
1311
  }
1304
1312
  function runManagedAuthDiscardedProviderSession(action) {
1305
1313
  return managedAuthAttemptStorage.run({ kind: "discard_provider_session" }, action);
@@ -1308,6 +1316,18 @@ function currentManagedAuthAttemptId() {
1308
1316
  const context = managedAuthAttemptStorage.getStore();
1309
1317
  return context?.kind === "isolated_transaction" ? context.transactionId : null;
1310
1318
  }
1319
+ function currentManagedAuthProviderId() {
1320
+ const context = managedAuthAttemptStorage.getStore();
1321
+ return context?.kind === "isolated_transaction" || context?.kind === "provider" ? context.providerId : "credential";
1322
+ }
1323
+ function recordCurrentManagedAuthSession(authSessionId) {
1324
+ const context = managedAuthAttemptStorage.getStore();
1325
+ if (context?.kind === "isolated_transaction") context.createdAuthSessionId = authSessionId;
1326
+ }
1327
+ function currentManagedAuthCreatedSessionId() {
1328
+ const context = managedAuthAttemptStorage.getStore();
1329
+ return context?.kind === "isolated_transaction" ? context.createdAuthSessionId : null;
1330
+ }
1311
1331
  function shouldDiscardCurrentManagedAuthProviderSession() {
1312
1332
  return managedAuthAttemptStorage.getStore()?.kind === "discard_provider_session";
1313
1333
  }
@@ -1320,6 +1340,12 @@ function managedAuthUserCreateOverride(settings, user) {
1320
1340
  if (managedAuthRequiresEmailVerification(settings)) return void 0;
1321
1341
  return { data: { ...user, emailVerified: true } };
1322
1342
  }
1343
+ function managedAuthUserCreateAdmission(settings, user, providerId) {
1344
+ if (managedAuthRequiresEmailVerification(settings) && providerId !== "credential" && !user.emailVerified) {
1345
+ return false;
1346
+ }
1347
+ return managedAuthUserCreateOverride(settings, user);
1348
+ }
1323
1349
  async function hashManagedAuthPassword(password) {
1324
1350
  return await hashPassword(password);
1325
1351
  }
@@ -1432,7 +1458,24 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1432
1458
  },
1433
1459
  accountLinking: {
1434
1460
  enabled: false
1435
- }
1461
+ },
1462
+ encryptOAuthTokens: true,
1463
+ storeStateStrategy: "database"
1464
+ },
1465
+ socialProviders: {
1466
+ ...settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret ? {
1467
+ google: {
1468
+ clientId: settings.managedAuthGoogleClientId,
1469
+ clientSecret: settings.managedAuthGoogleClientSecret,
1470
+ prompt: "select_account"
1471
+ }
1472
+ } : {},
1473
+ ...settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret ? {
1474
+ github: {
1475
+ clientId: settings.managedAuthGithubClientId,
1476
+ clientSecret: settings.managedAuthGithubClientSecret
1477
+ }
1478
+ } : {}
1436
1479
  },
1437
1480
  verification: {
1438
1481
  modelName: "auth_verifications",
@@ -1497,6 +1540,7 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1497
1540
  session: {
1498
1541
  create: {
1499
1542
  before: async (session) => {
1543
+ const providerId = currentManagedAuthProviderId();
1500
1544
  await ensureCanonicalHumanIdentityForAuthUser(db, session.userId);
1501
1545
  const preflightProjection = await getCanonicalHumanIdentityProjection(
1502
1546
  db,
@@ -1510,7 +1554,7 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1510
1554
  if (!preflight.allowed) {
1511
1555
  const exactRecoveryBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
1512
1556
  authUserId: session.userId,
1513
- providerId: "credential"
1557
+ providerId
1514
1558
  });
1515
1559
  const recoveryBinding = preflightProjection.loginBindings.find(
1516
1560
  (binding) => binding.id === exactRecoveryBinding.id && binding.status === "recovery_pending"
@@ -1544,7 +1588,7 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1544
1588
  const projection = await getCanonicalHumanIdentityProjection(db, session.userId);
1545
1589
  const exactBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
1546
1590
  authUserId: session.userId,
1547
- providerId: "credential"
1591
+ providerId
1548
1592
  });
1549
1593
  const activeBinding = projection.loginBindings.find(
1550
1594
  (binding) => binding.id === exactBinding.id
@@ -1575,6 +1619,7 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1575
1619
  };
1576
1620
  },
1577
1621
  after: async (session) => {
1622
+ recordCurrentManagedAuthSession(session.id);
1578
1623
  if (!shouldDiscardCurrentManagedAuthProviderSession()) return;
1579
1624
  await db.execute(sql`delete from auth_sessions where id = ${session.id}`);
1580
1625
  }
@@ -1582,7 +1627,7 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1582
1627
  },
1583
1628
  user: {
1584
1629
  create: {
1585
- before: async (user) => managedAuthUserCreateOverride(settings, user),
1630
+ before: async (user) => managedAuthUserCreateAdmission(settings, user, currentManagedAuthProviderId()),
1586
1631
  after: async (user) => {
1587
1632
  if (!user.emailVerified) return;
1588
1633
  await ensureManagedAccessForUser(db, {
@@ -1598,6 +1643,70 @@ function createManagedAuth(settings, db, managedEmailTransport) {
1598
1643
  }
1599
1644
  });
1600
1645
  }
1646
+ async function resolveManagedAuthOAuthAttempt(auth, request, provider, publicBaseUrl) {
1647
+ const expectedOrigin = canonicalPublicOrigin(publicBaseUrl);
1648
+ if (!expectedOrigin) return null;
1649
+ const state = new URL(request.url).searchParams.get("state");
1650
+ if (!state) return null;
1651
+ const verification = await (await auth.$context).internalAdapter.findVerificationValue(state);
1652
+ if (!verification?.value) return null;
1653
+ let parsed;
1654
+ try {
1655
+ parsed = JSON.parse(verification.value);
1656
+ } catch {
1657
+ return null;
1658
+ }
1659
+ if (!parsed || typeof parsed !== "object") return null;
1660
+ const stateData = parsed;
1661
+ const proof = stateData.opengeniManagedAuth;
1662
+ if (!proof || typeof proof !== "object") return null;
1663
+ const value = proof;
1664
+ if (value.version !== 1 || value.provider !== provider || typeof value.transactionId !== "string" || typeof value.authorityHash !== "string" || typeof value.transactionSecretHash !== "string" || typeof value.expectedGeneration !== "string" || typeof value.expectedActorEpoch !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
1665
+ value.transactionId
1666
+ ) || !/^[0-9a-f]{64}$/u.test(value.authorityHash) || !/^[0-9a-f]{64}$/u.test(value.transactionSecretHash) || !/^[1-9][0-9]*$/u.test(value.expectedGeneration) || !/^[1-9][0-9]*$/u.test(value.expectedActorEpoch)) {
1667
+ return null;
1668
+ }
1669
+ const callbackURL = typeof stateData.callbackURL === "string" ? stateData.callbackURL : null;
1670
+ const errorURL = typeof stateData.errorURL === "string" ? stateData.errorURL : null;
1671
+ if (!callbackURL || !errorURL || !managedAuthOAuthReturnMatches(callbackURL, expectedOrigin, value.transactionId, "complete") || !managedAuthOAuthReturnMatches(errorURL, expectedOrigin, value.transactionId, "error")) {
1672
+ return null;
1673
+ }
1674
+ return {
1675
+ transactionId: value.transactionId,
1676
+ provider,
1677
+ authorityHash: value.authorityHash,
1678
+ transactionSecretHash: value.transactionSecretHash,
1679
+ expectedGeneration: value.expectedGeneration,
1680
+ expectedActorEpoch: value.expectedActorEpoch
1681
+ };
1682
+ }
1683
+ async function isolatedManagedAuthOAuthCallbackRequest(auth, request) {
1684
+ const context = await auth.$context;
1685
+ const stateCookieName = context.createAuthCookie("state").name;
1686
+ const headers = new Headers(request.headers);
1687
+ const stateCookie2 = cookiePair(headers.get("cookie"), stateCookieName);
1688
+ if (stateCookie2) headers.set("cookie", stateCookie2);
1689
+ else headers.delete("cookie");
1690
+ headers.delete("authorization");
1691
+ headers.delete("x-forwarded-user");
1692
+ return { request: new Request(request, { headers }), stateCookieName };
1693
+ }
1694
+ function managedAuthOAuthReturnMatches(raw, expectedOrigin, transactionId, outcome) {
1695
+ try {
1696
+ const url = new URL(raw);
1697
+ return url.origin === expectedOrigin && url.pathname === "/account-auth" && url.searchParams.size === 2 && url.searchParams.get("transaction") === transactionId && url.searchParams.get("social") === outcome && !url.hash;
1698
+ } catch {
1699
+ return false;
1700
+ }
1701
+ }
1702
+ function cookiePair(header, name) {
1703
+ for (const part of header?.split(";") ?? []) {
1704
+ const separator = part.indexOf("=");
1705
+ if (separator < 0 || part.slice(0, separator).trim() !== name) continue;
1706
+ return `${name}=${part.slice(separator + 1).trim()}`;
1707
+ }
1708
+ return null;
1709
+ }
1601
1710
  function betterAuthBaseUrl(settings) {
1602
1711
  const allowedHosts = splitCsv(settings.betterAuthAllowedHosts);
1603
1712
  if (allowedHosts.length === 0) {
@@ -1647,8 +1756,12 @@ function escapeHtml(value) {
1647
1756
 
1648
1757
  // src/app.ts
1649
1758
  import {
1759
+ adoptManagedAuthSession,
1650
1760
  MANAGED_AUTH_SESSION_SET_COOKIE as MANAGED_AUTH_SESSION_SET_COOKIE2,
1651
1761
  ManagedAuthActorChangeError as ManagedAuthActorChangeError2,
1762
+ managedAuthCsrfHash as managedAuthCsrfHash2,
1763
+ managedAuthDerivedUuid as managedAuthDerivedUuid2,
1764
+ managedAuthSecretRequestDigest as managedAuthSecretRequestDigest2,
1652
1765
  managedAuthSha256 as managedAuthSha2562
1653
1766
  } from "@opengeni/core/managed-auth-session-sets";
1654
1767
 
@@ -1662,6 +1775,7 @@ function createBetterAuthSessionAdapter(auth, db) {
1662
1775
  }
1663
1776
  const result = await runManagedAuthAttempt(
1664
1777
  input.transactionId,
1778
+ "credential",
1665
1779
  async () => await auth.api.signInEmail({
1666
1780
  body: {
1667
1781
  email: input.credentials.email,
@@ -2145,6 +2259,7 @@ import {
2145
2259
  stableJson,
2146
2260
  compactSessionEventResult as compactSessionEventResult2,
2147
2261
  sessionEventLatestClassToSemanticClass,
2262
+ MemorySlackPublicationDistribution,
2148
2263
  SessionMcpCredentialUpdateInput,
2149
2264
  ToolAuthNeededPayload,
2150
2265
  VariableSetVariableName,
@@ -2157,6 +2272,7 @@ import {
2157
2272
  SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
2158
2273
  SESSION_GOAL_TEXT_MAX_BYTES,
2159
2274
  SESSION_INSTRUCTIONS_MAX_CHARACTERS,
2275
+ SESSION_TITLE_MAX_CHARACTERS,
2160
2276
  MAX_SELECTED_VARIABLE_SETS,
2161
2277
  sessionGoalUtf8Bytes,
2162
2278
  TASK_NOTE_LIST_DEFAULT_LIMIT,
@@ -2205,6 +2321,8 @@ import {
2205
2321
  readVariableSetSecretAtomically,
2206
2322
  recordSyncedSocialPosts,
2207
2323
  listVariableSets,
2324
+ MEMORY_CORRECT_TOOL_DESCRIPTION,
2325
+ MEMORY_SAVE_TOOL_DESCRIPTION,
2208
2326
  MEMORY_SEARCH_TOOL_DESCRIPTION,
2209
2327
  requireScheduledTask,
2210
2328
  requireSession,
@@ -2234,7 +2352,11 @@ import {
2234
2352
  acceptSessionHumanInputResponse,
2235
2353
  HumanInputResponseValidationError
2236
2354
  } from "@opengeni/db";
2237
- import { appendAndPublishTurnEventsFenced, publishDurableSessionEvents } from "@opengeni/events";
2355
+ import {
2356
+ appendAndPublishEvents as appendAndPublishEvents3,
2357
+ appendAndPublishTurnEventsFenced,
2358
+ publishDurableSessionEvents
2359
+ } from "@opengeni/events";
2238
2360
  import { allowedFirstPartyMcpToolsForSession, codemodeWorkspaceUrl } from "@opengeni/config";
2239
2361
  import {
2240
2362
  createSignedState as createSignedState6,
@@ -2252,12 +2374,14 @@ import {
2252
2374
  authorizedSocialConnectionsForGrant,
2253
2375
  authorizedAtlassianConnectionsForGrant,
2254
2376
  buildCapabilityCatalog,
2377
+ correctWorkspaceMemoryWithSlackPublication,
2255
2378
  nativeConnectionCapabilityRecommendations,
2256
2379
  requireLiveAgentAttemptAuthorization as requireLiveAgentAttemptAuthorization2,
2257
2380
  requireSessionAuthorization as requireSessionAuthorization2,
2258
2381
  requireSessionAuthorizationListScope,
2259
2382
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError3,
2260
2383
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError2,
2384
+ saveWorkspaceMemoryWithSlackPublication,
2261
2385
  searchCapabilityCatalogItems
2262
2386
  } from "@opengeni/core";
2263
2387
  import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
@@ -2502,6 +2626,8 @@ import {
2502
2626
  withRunCredentialsSession
2503
2627
  } from "@opengeni/runtime/sandbox";
2504
2628
  import {
2629
+ managedSessionGroupBackend,
2630
+ managedSessionGroupOs,
2505
2631
  providerSettingsForSessionSandboxRuntime,
2506
2632
  relayConfigFromSettings,
2507
2633
  resolveSessionSandboxRuntime,
@@ -3079,15 +3205,19 @@ async function withChannelAOperation(services, ctx, readOnly, fn) {
3079
3205
  ctx.waitSignal?.throwIfAborted();
3080
3206
  const pointer = await readActiveSandbox(db, workspaceId, session.id);
3081
3207
  if (!pointer?.activeSandboxId) {
3082
- if (settings.sandboxBackend !== "none" && settings.sandboxBackend !== "selfhosted") {
3208
+ const groupBackend = managedSessionGroupBackend(
3209
+ settings.sandboxBackend,
3210
+ session.sandboxBackend
3211
+ );
3212
+ if (groupBackend) {
3083
3213
  return await withChannelAOperation(
3084
3214
  services,
3085
3215
  {
3086
3216
  ...ctx,
3087
3217
  session: {
3088
3218
  ...session,
3089
- sandboxBackend: settings.sandboxBackend,
3090
- sandboxOs: "linux"
3219
+ sandboxBackend: groupBackend,
3220
+ sandboxOs: managedSessionGroupOs(session.sandboxBackend, session.sandboxOs)
3091
3221
  }
3092
3222
  },
3093
3223
  readOnly,
@@ -3095,7 +3225,7 @@ async function withChannelAOperation(services, ctx, readOnly, fn) {
3095
3225
  );
3096
3226
  }
3097
3227
  throw new HTTPException2(409, {
3098
- message: "machine-home session has no active Connected Machine"
3228
+ message: "machine-home session has no active Connected Machine or managed sandbox"
3099
3229
  });
3100
3230
  }
3101
3231
  const sandbox = await getSandbox(
@@ -4782,6 +4912,7 @@ import {
4782
4912
  loadIntegrationOAuthClient,
4783
4913
  normalizeBearerScheme,
4784
4914
  replaceIntegrationOAuthClientIfCurrent,
4915
+ resolveNamedManagedPersonalWorkspaceGrant,
4785
4916
  storeIntegrationOAuthClient,
4786
4917
  updateConnection,
4787
4918
  withDatabaseStatementTimeout
@@ -5502,7 +5633,8 @@ function requireIntegrationsStateSecret(settings) {
5502
5633
  return secret;
5503
5634
  }
5504
5635
  async function requireOAuthCallbackGrant(db, state) {
5505
- const grant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
5636
+ const membershipGrant = await getWorkspaceGrant(db, state.subjectId, state.workspaceId);
5637
+ const grant = membershipGrant?.accountId === state.accountId ? membershipGrant : state.personalOwnerVerified ? await resolveNamedManagedPersonalWorkspaceGrant(db, state) : null;
5506
5638
  if (!grant || grant.accountId !== state.accountId || !hasPermission3(grant.permissions, "connections:write")) {
5507
5639
  throw new HTTPException8(403, {
5508
5640
  message: "OAuth subject no longer has permission to write this workspace connection"
@@ -7528,6 +7660,7 @@ import {
7528
7660
  ScheduledTaskSyncError,
7529
7661
  syncCreatedScheduledTask as syncCreatedScheduledTask3,
7530
7662
  syncUpdatedScheduledTask as syncUpdatedScheduledTask3,
7663
+ validateScheduledTaskMachineTarget,
7531
7664
  validateScheduledTaskTarget,
7532
7665
  updateScheduledTaskForApi,
7533
7666
  validatedScheduledTaskUpdate
@@ -8211,7 +8344,32 @@ var SESSION_WAIT_EVENT_TYPES = [
8211
8344
  "goal.cleared",
8212
8345
  "goal.continuation"
8213
8346
  ];
8214
- var SESSION_WAIT_EVENT_TYPE_SET = new Set(SESSION_WAIT_EVENT_TYPES);
8347
+ var SESSION_WAIT_COMPLETION_EVENT_TYPES = [
8348
+ "turn.completed",
8349
+ "turn.failed",
8350
+ "turn.cancelled",
8351
+ "turn.superseded",
8352
+ "turn.capacity_waiting",
8353
+ "session.requiresAction",
8354
+ "session.humanInput.requested",
8355
+ "session.control.paused",
8356
+ "tool.auth_needed",
8357
+ "credential.auth_needed",
8358
+ "rig.setup.failed",
8359
+ "goal.paused"
8360
+ ];
8361
+ var SESSION_WAIT_COMPLETION_EVENT_TYPE_SET = new Set(
8362
+ SESSION_WAIT_COMPLETION_EVENT_TYPES
8363
+ );
8364
+ function sessionWaitCompletionEventMatches(event) {
8365
+ if (!SESSION_WAIT_COMPLETION_EVENT_TYPE_SET.has(event.type)) return false;
8366
+ if (event.type !== "turn.completed") return true;
8367
+ if (event.payload === null || typeof event.payload !== "object" || Array.isArray(event.payload)) {
8368
+ return false;
8369
+ }
8370
+ const payload = event.payload;
8371
+ return Object.prototype.hasOwnProperty.call(payload, "output") && !Object.prototype.hasOwnProperty.call(payload, "segmentLimit") && !Object.prototype.hasOwnProperty.call(payload, "maintenance");
8372
+ }
8215
8373
  var SESSION_WAIT_OWN_PENDING_EVENT_TYPE = "system.update.pending";
8216
8374
  function ownPendingKindWakes(kind) {
8217
8375
  const wakeClass = SESSION_SYSTEM_UPDATE_WAKE_CLASS[kind];
@@ -8239,6 +8397,10 @@ async function waitForSessionChanges(input) {
8239
8397
  let liveFanout = true;
8240
8398
  let waited = false;
8241
8399
  const ownSessionId = input.source.readOwnPendingUpdateKinds ? input.ownSessionId : null;
8400
+ const targetEventTypeSet = new Set(
8401
+ input.targetEventTypes ?? SESSION_WAIT_EVENT_TYPES
8402
+ );
8403
+ const targetEventMatches = (event) => targetEventTypeSet.has(event.type) && (input.targetEventMatches?.(event) ?? true);
8242
8404
  const targetAfter = /* @__PURE__ */ new Map();
8243
8405
  for (const target of input.targets) {
8244
8406
  const existing = targetAfter.get(target.sessionId);
@@ -8259,7 +8421,7 @@ async function waitForSessionChanges(input) {
8259
8421
  const after = targetAfter.get(sessionId);
8260
8422
  for (const event of events) {
8261
8423
  if (event.sessionId !== sessionId) continue;
8262
- if (after !== void 0 && event.sequence > after && SESSION_WAIT_EVENT_TYPE_SET.has(event.type)) {
8424
+ if (after !== void 0 && event.sequence > after && targetEventMatches(event)) {
8263
8425
  return true;
8264
8426
  }
8265
8427
  if (sessionId === ownSessionId && event.type === SESSION_WAIT_OWN_PENDING_EVENT_TYPE) {
@@ -8290,7 +8452,7 @@ async function waitForSessionChanges(input) {
8290
8452
  const changed = [];
8291
8453
  for (const { target, read } of targetReads) {
8292
8454
  const events = read.events.filter(
8293
- (event) => event.sequence > target.afterSequence && SESSION_WAIT_EVENT_TYPE_SET.has(event.type)
8455
+ (event) => event.sequence > target.afterSequence && targetEventMatches(event)
8294
8456
  );
8295
8457
  if (events.length === 0) continue;
8296
8458
  changed.push({
@@ -9119,6 +9281,8 @@ import {
9119
9281
  renewSandboxProviderExpiration
9120
9282
  } from "@opengeni/runtime/sandbox";
9121
9283
  import {
9284
+ managedSessionGroupBackend as managedSessionGroupBackend2,
9285
+ managedSessionGroupOs as managedSessionGroupOs2,
9122
9286
  providerSettingsForSessionSandboxRuntime as providerSettingsForSessionSandboxRuntime2,
9123
9287
  relayConfigFromSettings as relayConfigFromSettings2,
9124
9288
  resolveSessionSandboxRuntime as resolveSessionSandboxRuntime2
@@ -9178,7 +9342,21 @@ async function sessionAttachEnvironment(services, workspaceId, session, attachSu
9178
9342
  }
9179
9343
  async function attachViewer(services, input) {
9180
9344
  const { db, settings } = services;
9181
- const { accountId, workspaceId, session } = input;
9345
+ const { accountId, workspaceId } = input;
9346
+ const groupBackend = managedSessionGroupBackend2(
9347
+ settings.sandboxBackend,
9348
+ input.session.sandboxBackend
9349
+ );
9350
+ if (!groupBackend) {
9351
+ throw new HTTPException10(409, {
9352
+ message: "session has no managed sandbox group to attach"
9353
+ });
9354
+ }
9355
+ const session = groupBackend === input.session.sandboxBackend ? input.session : {
9356
+ ...input.session,
9357
+ sandboxBackend: groupBackend,
9358
+ sandboxOs: managedSessionGroupOs2(input.session.sandboxBackend, input.session.sandboxOs)
9359
+ };
9182
9360
  const viewerId = input.viewerId ?? crypto.randomUUID();
9183
9361
  const attachSubjectId = claimableSubjectId(input.viewerSubjectId ?? null);
9184
9362
  const attachAuthorityEpoch = attachSubjectId ? await getSessionAuthorityEpoch(db, { accountId, workspaceId, sessionId: session.id }) : null;
@@ -15872,17 +16050,19 @@ function mutationAnnotations(title, options) {
15872
16050
 
15873
16051
  // src/mcp/company-brain-governed-writes.ts
15874
16052
  import {
15875
- AGENT_AUTHORED_DURABLE_TEXT_STYLE,
15876
16053
  AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS,
15877
16054
  AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_TOO_LONG_MESSAGE,
16055
+ AGENT_AUTHORED_INSTRUCTION_POLICY_STYLE,
15878
16056
  AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS,
15879
16057
  AGENT_AUTHORED_PREFERENCE_CONTENT_TOO_LONG_MESSAGE,
16058
+ AGENT_AUTHORED_SKILL_STYLE,
15880
16059
  PREFERENCE_REGISTRY_DESCRIPTOR_DESCRIPTION_MAX_CHARS,
15881
16060
  PREFERENCE_REGISTRY_STABLE_KEY_MAX_CHARS,
15882
16061
  PREFERENCE_REGISTRY_TITLE_MAX_CHARS,
15883
16062
  WorkspaceInstructionPolicyTarget
15884
16063
  } from "@opengeni/contracts";
15885
16064
  import { createCompanyBrainLearningPolicyRouter } from "@opengeni/core";
16065
+ import { PreferenceRegistryStableKeyConflictError } from "@opengeni/db";
15886
16066
  import * as z6 from "zod/v4";
15887
16067
  var reason = z6.string().trim().min(1).max(4096);
15888
16068
  var evidence = {
@@ -15903,6 +16083,20 @@ var taskNotePromotion = {
15903
16083
  };
15904
16084
  function registerCompanyBrainGovernedWriteTools(input) {
15905
16085
  const router = input.router ?? createCompanyBrainLearningPolicyRouter({ db: input.db });
16086
+ const writeResult = async (write) => {
16087
+ try {
16088
+ return input.json(await write());
16089
+ } catch (error) {
16090
+ if (error instanceof PreferenceRegistryStableKeyConflictError) {
16091
+ return input.json({
16092
+ status: "not_proposed",
16093
+ code: "preference_stable_key_conflict",
16094
+ message: error.message
16095
+ });
16096
+ }
16097
+ throw error;
16098
+ }
16099
+ };
15906
16100
  input.server.registerTool(
15907
16101
  "knowledge_propose",
15908
16102
  {
@@ -15911,8 +16105,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
15911
16105
  },
15912
16106
  async (request) => {
15913
16107
  await input.authorize();
15914
- return input.json(
15915
- await router.write({
16108
+ return writeResult(
16109
+ () => router.write({
15916
16110
  attempt: input.attempt,
15917
16111
  request: { kind: "propose_knowledge", ...request }
15918
16112
  })
@@ -15927,8 +16121,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
15927
16121
  },
15928
16122
  async (request) => {
15929
16123
  await input.authorize();
15930
- return input.json(
15931
- await router.write({
16124
+ return writeResult(
16125
+ () => router.write({
15932
16126
  attempt: input.attempt,
15933
16127
  request: { kind: "correct_knowledge", ...request }
15934
16128
  })
@@ -15943,8 +16137,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
15943
16137
  },
15944
16138
  async (request) => {
15945
16139
  await input.authorize();
15946
- return input.json(
15947
- await router.write({
16140
+ return writeResult(
16141
+ () => router.write({
15948
16142
  attempt: input.attempt,
15949
16143
  request: { kind: "promote_task_note_knowledge", ...request }
15950
16144
  })
@@ -15954,7 +16148,7 @@ function registerCompanyBrainGovernedWriteTools(input) {
15954
16148
  input.server.registerTool(
15955
16149
  "task_note_promote_instruction_policy",
15956
16150
  {
15957
- description: `Atomically promote one still-active note from this exact root task tree into an inactive workspace instruction-policy draft. The note bytes remain exact evidence and draft content. Use this only for a universal always-on rule, never for an incident, fact, decision, outcome, or conditional procedure. Once a human activates the draft, those bytes are composed verbatim into the prompt of every session the target applies to, so a note over ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters is rejected here rather than truncated: write a fresh minimal imperative note instead of promoting a long working note. The frozen learning policy records a decision receipt, but mandatory policy still requires human activation even under Automatic; this never widens scope.`,
16151
+ description: `Atomically promote one still-active note from this exact root task tree into a workspace instruction-policy proposal. The note bytes remain exact evidence and draft content. Use this only for a universal always-on rule, never for an incident, fact, decision, outcome, or conditional procedure. Once active, those bytes are composed verbatim into the prompt of every session the target applies to, so a note over ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters is rejected here rather than truncated: write a fresh minimal imperative note instead of promoting a long working note. Off creates nothing; Review first keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt. This never widens scope.`,
15958
16152
  inputSchema: {
15959
16153
  ...taskNotePromotion,
15960
16154
  target: WorkspaceInstructionPolicyTarget,
@@ -15964,8 +16158,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
15964
16158
  },
15965
16159
  async (request) => {
15966
16160
  await input.authorize();
15967
- return input.json(
15968
- await router.write({
16161
+ return writeResult(
16162
+ () => router.write({
15969
16163
  attempt: input.attempt,
15970
16164
  request: { kind: "promote_task_note_instruction_policy", ...request }
15971
16165
  })
@@ -15989,8 +16183,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
15989
16183
  },
15990
16184
  async (request) => {
15991
16185
  await input.authorize();
15992
- return input.json(
15993
- await router.write({
16186
+ return writeResult(
16187
+ () => router.write({
15994
16188
  attempt: input.attempt,
15995
16189
  request: { kind: "promote_task_note_preference", ...request }
15996
16190
  })
@@ -16000,7 +16194,7 @@ function registerCompanyBrainGovernedWriteTools(input) {
16000
16194
  input.server.registerTool(
16001
16195
  "instruction_policy_propose",
16002
16196
  {
16003
- description: `Materialize an evidence-backed inactive workspace instruction-policy draft. Use this only for a minimal universal rule, never for an incident, fact, decision, outcome, or conditional procedure. Once a human activates it, this content is composed verbatim into the prompt of every session the target applies to (every session in this workspace for a global charter or policy, every session bound to the role for a role policy), so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_DURABLE_TEXT_STYLE} The frozen learning policy records a decision receipt, but this tool cannot activate mandatory behavior, including when learning mode is Automatic; a human must activate the draft.`,
16197
+ description: `Materialize an evidence-backed workspace instruction-policy proposal. Use this only for a minimal universal rule, never for an incident, fact, decision, outcome, or conditional procedure. Once active, this content is composed verbatim into the prompt of every session the target applies to (every session in this workspace for a global charter or policy, every session bound to the role for a role policy), so keep it under ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_INSTRUCTION_POLICY_STYLE} Off creates nothing; Review first keeps the proposal inactive; Autonomous may activate an eligible proposal through the governed instruction lifecycle with an undoable receipt.`,
16004
16198
  inputSchema: {
16005
16199
  ...evidence,
16006
16200
  target: WorkspaceInstructionPolicyTarget,
@@ -16015,8 +16209,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
16015
16209
  },
16016
16210
  async (request) => {
16017
16211
  await input.authorize();
16018
- return input.json(
16019
- await router.write({
16212
+ return writeResult(
16213
+ () => router.write({
16020
16214
  attempt: input.attempt,
16021
16215
  request: { kind: "propose_instruction_policy", ...request }
16022
16216
  })
@@ -16026,7 +16220,7 @@ function registerCompanyBrainGovernedWriteTools(input) {
16026
16220
  input.server.registerTool(
16027
16221
  "preference_propose",
16028
16222
  {
16029
- description: `Materialize an evidence-backed workspace Skill proposal in the structured preference authority. Use this only for reusable conditional how-to guidance, never for an incident, fact, decision, outcome, or universal always-on rule. Its short title and description are what get composed into every session prompt; the content is retrieved on demand, so its length is retrieval cost rather than standing prompt cost. Keep the content under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_DURABLE_TEXT_STYLE} Under Suggest it stays inactive for human review; under Automatic an eligible decision is activated through the governed preference lifecycle with an undoable receipt. It never creates mandatory authority.`,
16223
+ description: `Materialize an evidence-backed workspace Skill proposal in the structured preference authority. Use this only for reusable conditional how-to guidance, never for an incident, fact, decision, outcome, or universal always-on rule. Its short title and description are what get composed into every session prompt; the content is retrieved on demand, so its length is retrieval cost rather than standing prompt cost. Keep the content under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS} characters. ${AGENT_AUTHORED_SKILL_STYLE} Under Suggest it stays inactive for human review; under Automatic an eligible decision is activated through the governed preference lifecycle with an undoable receipt. It never creates mandatory authority.`,
16030
16224
  inputSchema: {
16031
16225
  ...evidence,
16032
16226
  stableKey: z6.string().trim().min(1).max(PREFERENCE_REGISTRY_STABLE_KEY_MAX_CHARS),
@@ -16045,8 +16239,8 @@ function registerCompanyBrainGovernedWriteTools(input) {
16045
16239
  },
16046
16240
  async (request) => {
16047
16241
  await input.authorize();
16048
- return input.json(
16049
- await router.write({
16242
+ return writeResult(
16243
+ () => router.write({
16050
16244
  attempt: input.attempt,
16051
16245
  request: { kind: "propose_preference", ...request }
16052
16246
  })
@@ -16090,7 +16284,7 @@ function registerCompanyProfileAgentAdminTools(input) {
16090
16284
  input.server.registerTool(
16091
16285
  "company_profile_propose",
16092
16286
  {
16093
- description: `Prepare the organization's small, stable identity: identity says who the organization is, and mission says why it exists. Once activated, both fields are mandatory prompt context in every root session for the whole organization, so use one plain descriptive statement per field with no products, customers, goals, constraints, procedures, or marketing copy. Those details belong in organization-scoped Documents and are retrieved only when relevant. Each field is bounded to ${AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS} characters for agent-authored proposals. This creates only an immutable inactive proposal for the exact live turn initiated by the organization owner and does not use workspace learning policy. The receipt returns the exact \`humanInput\` payload; call \`request_human_input\` with it verbatim, then call \`company_profile_confirm\` with the returned requestId.`,
16287
+ description: `Prepare the organization's small, stable identity: identity says who the organization is, and mission says why it exists. Once activated, both fields are mandatory prompt context in every root session for the whole organization, so use one plain descriptive statement per field with no products, customers, goals, constraints, procedures, or marketing copy. Those details belong in organization-scoped Documents and are retrieved only when relevant. Each field is bounded to ${AGENT_AUTHORED_COMPANY_PROFILE_SCALAR_MAX_CHARS} characters for agent-authored proposals. This is independent of workspace learning policy and follows the organization owner's Agent-managed identity mode. Off creates nothing. Review first returns status=confirmation_required with the exact \`humanInput\` payload; call \`request_human_input\` with it verbatim, then call \`company_profile_confirm\` with the returned requestId. Autonomous may return status=activated immediately after exact live-owner, stale-head, and compare-and-swap checks; do not ask for another confirmation in that case.`,
16094
16288
  inputSchema: {
16095
16289
  operationId: z7.string().uuid(),
16096
16290
  identity: scalar,
@@ -16170,6 +16364,7 @@ import {
16170
16364
  agentAuthoredDurableTextTooLongMessage
16171
16365
  } from "@opengeni/contracts";
16172
16366
  import { RememberError, createRememberRouter } from "@opengeni/core";
16367
+ import { PreferenceRegistryStableKeyConflictError as PreferenceRegistryStableKeyConflictError2 } from "@opengeni/db";
16173
16368
  import * as z8 from "zod/v4";
16174
16369
  var laneContentMaxChars = {
16175
16370
  instruction_policy: AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS2,
@@ -16188,7 +16383,7 @@ function registerRememberTools(input) {
16188
16383
  input.server.registerTool(
16189
16384
  "remember",
16190
16385
  {
16191
- description: `Durably remember something the user explicitly asked to keep for this workspace. Route by purpose: lane=knowledge for a fact, decision, incident, bug fix, or outcome that should become searchable Memory; lane=preference creates a Skill for reusable conditional how-to guidance; lane=instruction_policy is only for a universal always/never rule that should apply to nearly every task. Write the instruction lane as the shortest complete rule, at most ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS2} characters and normally 1-3 imperative sentences, with no numbered steps, examples, rationale, or restated defaults. Keep a Skill under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS2} characters; only its one-sentence descriptor is composed and the full instructions are retrieved on demand. Do not copy one item into multiple lanes. Under Automatic learning a Skill may activate immediately; otherwise the receipt returns status=confirmation_required with the exact \`humanInput\` payload: call \`request_human_input\` with it verbatim, then call \`remember_confirm\` with the returned requestId. Workspace instructions and Memory always need that confirmation. Do not use this for facts you merely inferred; use knowledge_propose or task notes for those. Confirmed lane=knowledge content keeps its reviewed claim provenance and materializes its exact approved text into Memory for later \`memory_search\` retrieval.`,
16386
+ description: `Create governed durable knowledge, a Skill, or a mandatory workspace instruction. When the agent-only memory_save tool is available, use it instead for ordinary durable facts, decisions, incidents, bug fixes, and confirmed outcomes; those Memory writes are autonomous and independent of Learning mode. Use lane=knowledge only when memory_save is unavailable and the user explicitly requests reviewed workspace knowledge; lane=preference creates a Skill for reusable conditional how-to guidance; lane=instruction_policy is only for a universal always/never rule that should apply to nearly every task. Write the instruction lane as the shortest complete rule, at most ${AGENT_AUTHORED_INSTRUCTION_POLICY_CONTENT_MAX_CHARS2} characters and normally 1-3 imperative sentences, with no numbered steps, examples, rationale, or restated defaults. Keep a Skill under ${AGENT_AUTHORED_PREFERENCE_CONTENT_MAX_CHARS2} characters with one clear trigger and outcome, only the necessary steps, checks, and exceptions, and a one-sentence descriptor. Do not copy one item into multiple lanes. Under Autonomous learning, an eligible Skill or workspace instruction may activate immediately through its governed lifecycle. Under Review first, it remains inactive and the receipt returns status=confirmation_required with the exact \`humanInput\` payload: call \`request_human_input\` with it verbatim, then call \`remember_confirm\` with the returned requestId. Off creates no governed change. Reviewed Knowledge always needs confirmation. Do not use lane=knowledge for facts you merely inferred; use memory_save when available, otherwise knowledge_propose or task notes. Confirmed lane=knowledge content keeps its reviewed claim provenance and materializes its exact approved text into Memory for later \`memory_search\` retrieval.`,
16192
16387
  inputSchema: {
16193
16388
  lane: z8.enum(["preference", "instruction_policy", "knowledge"]),
16194
16389
  ...laneFields,
@@ -16220,9 +16415,20 @@ function registerRememberTools(input) {
16220
16415
  title: request.title ?? content.slice(0, 80),
16221
16416
  description: request.description ?? content.slice(0, 200)
16222
16417
  } : lane === "instruction_policy" ? { ...base, lane, target: request.target } : { ...base, lane, subject: request.subject ?? content.slice(0, 80) };
16223
- return input.json(
16224
- await router.remember({ attempt: input.attempt, request: rememberRequest })
16225
- );
16418
+ try {
16419
+ return input.json(
16420
+ await router.remember({ attempt: input.attempt, request: rememberRequest })
16421
+ );
16422
+ } catch (error) {
16423
+ if (error instanceof PreferenceRegistryStableKeyConflictError2) {
16424
+ return input.json({
16425
+ status: "not_remembered",
16426
+ code: "preference_stable_key_conflict",
16427
+ message: error.message
16428
+ });
16429
+ }
16430
+ throw error;
16431
+ }
16226
16432
  }
16227
16433
  );
16228
16434
  input.server.registerTool(
@@ -18450,6 +18656,7 @@ function observeWorkDiscovery(observability, observation) {
18450
18656
  // src/mcp/server.ts
18451
18657
  var ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
18452
18658
  var ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1024;
18659
+ var MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS = WORK_DISCOVERY_QUERY_MAX_CHARS * 8;
18453
18660
  function boundedOrchestrationFailureMessage(value) {
18454
18661
  const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
18455
18662
  if (!normalized) return "OpenGeni could not complete the request.";
@@ -18534,9 +18741,6 @@ var FIRST_PARTY_TOOL_AUTHORIZATION = {
18534
18741
  goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
18535
18742
  goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
18536
18743
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
18537
- // Retired: never registered, so these are never consulted. The map must stay
18538
- // total over the tool-name union, which still carries both names so that
18539
- // previously written scheduled-task snapshots keep parsing.
18540
18744
  memory_save: { sessionRequired: true, allOf: ["documents:search"] },
18541
18745
  memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
18542
18746
  preference_registry_summary: {
@@ -19699,6 +19903,14 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
19699
19903
  agentConfig: task.agentConfig,
19700
19904
  missingTargetStatus: 404
19701
19905
  });
19906
+ await validateScheduledTaskMachineTarget({
19907
+ settings: deps.settings,
19908
+ db: deps.db,
19909
+ grant,
19910
+ runMode: task.runMode,
19911
+ agentConfig: task.agentConfig,
19912
+ requireOnline: true
19913
+ });
19702
19914
  await requireLimit3(deps, {
19703
19915
  accountId: grant.accountId,
19704
19916
  workspaceId: grant.workspaceId,
@@ -21103,6 +21315,7 @@ function registerPreferenceRegistryTools(server, deps, grant, json) {
21103
21315
  );
21104
21316
  }
21105
21317
  var MemoryKindSchema = z43.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
21318
+ var MemoryWriteKindSchema = z43.enum(["semantic", "decision", "episodic"]);
21106
21319
  function scheduledTaskReceipt(operation, task, outcome, changed, options = {}) {
21107
21320
  return mcpMutationReceipt({
21108
21321
  operation,
@@ -21150,11 +21363,34 @@ function scheduledTaskUpdateChangesState(task, update) {
21150
21363
  if (update.personalConnectionDelegations !== void 0) return true;
21151
21364
  return false;
21152
21365
  }
21366
+ function memorySlackPublicationActor(actor, sessionId, fallbackOwnerLabel) {
21367
+ return {
21368
+ actor: {
21369
+ kind: actor.initiator.kind === "subject" ? "human" : "service",
21370
+ subjectId: actor.initiator.subjectId,
21371
+ initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
21372
+ sessionId,
21373
+ turnId: actor.turnId,
21374
+ attemptId: actor.attemptId
21375
+ },
21376
+ ownerLabel: actor.initiator.label ?? fallbackOwnerLabel
21377
+ };
21378
+ }
21379
+ function memoryPreview(text) {
21380
+ const normalized = text.replace(/\s+/g, " ").trim();
21381
+ return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}\u2026`;
21382
+ }
21153
21383
  function registerMemoryTools(server, deps, grant, sessionId, json, promptMode) {
21384
+ const publicationInputSchema = z43.object({
21385
+ importance: z43.enum(["major", "normal", "minor"]),
21386
+ audience: z43.literal("workspace"),
21387
+ slackMode: z43.enum(["auto", "review", "never"]),
21388
+ shareSummary: z43.string().trim().min(1).max(4096)
21389
+ });
21154
21390
  server.registerTool(
21155
21391
  "memory_search",
21156
21392
  {
21157
- description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference-kind records are excluded from this tool because structured preferences are the only behavioral authority. To save something the user explicitly asked to keep, use \`remember\`; for your own findings use task notes and their promotion tools.`,
21393
+ description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} All existing Memory kinds are searchable. Legacy preference and procedure records are historical context, not active instructions; Skills and workspace instructions remain the behavioral authorities. When workspace Memory is enabled, use memory_save autonomously for durable facts, decisions, incidents, fixes, and outcomes, and memory_correct when an existing record is wrong or outdated.`,
21158
21394
  inputSchema: {
21159
21395
  query: z43.string().min(1),
21160
21396
  kind: MemoryKindSchema.optional(),
@@ -21175,6 +21411,195 @@ function registerMemoryTools(server, deps, grant, sessionId, json, promptMode) {
21175
21411
  )
21176
21412
  })
21177
21413
  );
21414
+ if (exactAgentAttemptClaims(grant) === null) return;
21415
+ server.registerTool(
21416
+ "memory_save",
21417
+ {
21418
+ description: MEMORY_SAVE_TOOL_DESCRIPTION,
21419
+ inputSchema: {
21420
+ text: z43.string().min(1),
21421
+ kind: MemoryWriteKindSchema,
21422
+ confidence: z43.number().min(0).max(1).optional(),
21423
+ replaces_id: z43.string().min(1).optional(),
21424
+ slack_publication: publicationInputSchema.optional()
21425
+ }
21426
+ },
21427
+ async ({ text, kind, confidence, replaces_id, slack_publication }) => {
21428
+ const actor = await requireLiveAgentAttemptAuthorization2(deps.db, grant, sessionId);
21429
+ const result = await saveWorkspaceMemoryWithSlackPublication(
21430
+ deps.db,
21431
+ {
21432
+ accountId: grant.accountId,
21433
+ workspaceId: grant.workspaceId,
21434
+ sessionId,
21435
+ text,
21436
+ kind,
21437
+ ...confidence !== void 0 ? { confidence } : {},
21438
+ ...replaces_id ? { replacesId: replaces_id } : {},
21439
+ origin: "agent"
21440
+ },
21441
+ slack_publication ? {
21442
+ distribution: MemorySlackPublicationDistribution.parse(slack_publication),
21443
+ actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null).actor,
21444
+ ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null
21445
+ } : null,
21446
+ deps.getDocumentServices().embedder
21447
+ );
21448
+ let timelineWarning = null;
21449
+ try {
21450
+ await appendAndPublishEvents3(deps.db, deps.bus, grant.workspaceId, sessionId, [
21451
+ {
21452
+ type: "memory.saved",
21453
+ payload: {
21454
+ memoryId: result.memory.id,
21455
+ kind: result.memory.kind,
21456
+ preview: memoryPreview(result.memory.text),
21457
+ deduped: result.deduped,
21458
+ ...result.superseded ? { supersededMemoryId: result.superseded.id } : {}
21459
+ }
21460
+ }
21461
+ ]);
21462
+ } catch {
21463
+ timelineWarning = "Memory committed, but its session timeline event could not be recorded.";
21464
+ console.warn("workspace memory save: committed without session timeline event", {
21465
+ errorClass: "MemoryTimelineOperationError",
21466
+ errorCode: "memory_save_timeline_append_failed",
21467
+ origin: "api",
21468
+ workspaceId: grant.workspaceId,
21469
+ sessionId,
21470
+ memoryId: result.memory.id
21471
+ });
21472
+ }
21473
+ const changed = !result.deduped || result.updated || result.superseded !== null;
21474
+ const outcome = result.updated || result.superseded !== null ? "updated" : result.deduped ? "unchanged" : "created";
21475
+ return json(
21476
+ mcpMutationReceipt({
21477
+ operation: "memory_save",
21478
+ committed: true,
21479
+ outcome,
21480
+ changed,
21481
+ resource: {
21482
+ type: "knowledge_memory",
21483
+ id: result.memory.id,
21484
+ state: result.memory.status
21485
+ },
21486
+ relatedResources: result.superseded ? [
21487
+ {
21488
+ type: "knowledge_memory",
21489
+ id: result.superseded.id,
21490
+ state: result.superseded.status
21491
+ }
21492
+ ] : void 0,
21493
+ timestamp: result.memory.updatedAt,
21494
+ idempotency: { status: "not_supported" },
21495
+ warnings: [
21496
+ ...!result.embedded ? ["Memory committed without a vector embedding; keyword search remains available."] : [],
21497
+ ...timelineWarning ? [timelineWarning] : []
21498
+ ],
21499
+ facts: {
21500
+ deduped: result.deduped,
21501
+ dedupeReason: result.dedupeReason,
21502
+ updatedInPlace: result.updated,
21503
+ embedded: result.embedded,
21504
+ slackPublicationDecision: result.slackPublication.decision?.eligible ? "eligible" : result.slackPublication.decision?.reason ?? "not_requested",
21505
+ slackPublicationId: result.slackPublication.enqueue?.kind === "enqueued" || result.slackPublication.enqueue?.kind === "replayed" ? result.slackPublication.enqueue.publication.id : null,
21506
+ slackPublicationState: result.slackPublication.enqueue?.kind === "enqueued" || result.slackPublication.enqueue?.kind === "replayed" ? result.slackPublication.enqueue.publication.state : null
21507
+ }
21508
+ })
21509
+ );
21510
+ }
21511
+ );
21512
+ server.registerTool(
21513
+ "memory_correct",
21514
+ {
21515
+ description: MEMORY_CORRECT_TOOL_DESCRIPTION,
21516
+ inputSchema: {
21517
+ id: z43.string().min(1),
21518
+ reason: z43.string().min(1).optional(),
21519
+ replacement_text: z43.string().min(1).optional(),
21520
+ slack_publication: publicationInputSchema.optional()
21521
+ }
21522
+ },
21523
+ async ({ id, reason: reason2, replacement_text, slack_publication }) => {
21524
+ const actor = await requireLiveAgentAttemptAuthorization2(deps.db, grant, sessionId);
21525
+ const result = await correctWorkspaceMemoryWithSlackPublication(
21526
+ deps.db,
21527
+ {
21528
+ accountId: grant.accountId,
21529
+ workspaceId: grant.workspaceId,
21530
+ sessionId,
21531
+ id,
21532
+ ...reason2 ? { reason: reason2 } : {},
21533
+ ...replacement_text ? { replacementText: replacement_text } : {},
21534
+ origin: "agent"
21535
+ },
21536
+ slack_publication ? {
21537
+ distribution: MemorySlackPublicationDistribution.parse(slack_publication),
21538
+ actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null).actor,
21539
+ ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null
21540
+ } : null,
21541
+ deps.getDocumentServices().embedder
21542
+ );
21543
+ let timelineWarning = null;
21544
+ try {
21545
+ await appendAndPublishEvents3(deps.db, deps.bus, grant.workspaceId, sessionId, [
21546
+ {
21547
+ type: "memory.corrected",
21548
+ payload: {
21549
+ memoryId: result.memory.id,
21550
+ kind: result.memory.kind,
21551
+ preview: memoryPreview(result.memory.text),
21552
+ action: result.action,
21553
+ ...reason2 ? { reason: memoryPreview(reason2) } : {},
21554
+ ...result.replacement ? {
21555
+ replacementMemoryId: result.replacement.id,
21556
+ replacementPreview: memoryPreview(result.replacement.text)
21557
+ } : {}
21558
+ }
21559
+ }
21560
+ ]);
21561
+ } catch {
21562
+ timelineWarning = "Memory correction committed, but its session timeline event could not be recorded.";
21563
+ console.warn("workspace memory correction: committed without session timeline event", {
21564
+ errorClass: "MemoryTimelineOperationError",
21565
+ errorCode: "memory_correct_timeline_append_failed",
21566
+ origin: "api",
21567
+ workspaceId: grant.workspaceId,
21568
+ sessionId,
21569
+ memoryId: result.memory.id
21570
+ });
21571
+ }
21572
+ return json(
21573
+ mcpMutationReceipt({
21574
+ operation: "memory_correct",
21575
+ committed: true,
21576
+ outcome: "updated",
21577
+ changed: true,
21578
+ resource: {
21579
+ type: "knowledge_memory",
21580
+ id: result.memory.id,
21581
+ state: result.memory.status
21582
+ },
21583
+ relatedResources: result.replacement ? [
21584
+ {
21585
+ type: "knowledge_memory",
21586
+ id: result.replacement.id,
21587
+ state: result.replacement.status
21588
+ }
21589
+ ] : void 0,
21590
+ timestamp: (result.replacement ?? result.memory).updatedAt,
21591
+ idempotency: { status: "not_supported" },
21592
+ warnings: timelineWarning ? [timelineWarning] : [],
21593
+ facts: {
21594
+ correctionAction: result.action,
21595
+ slackPublicationDecision: result.slackPublication.decision?.eligible ? "eligible" : result.slackPublication.decision?.reason ?? "not_requested",
21596
+ slackPublicationId: result.slackPublication.enqueue?.kind === "enqueued" || result.slackPublication.enqueue?.kind === "replayed" ? result.slackPublication.enqueue.publication.id : null,
21597
+ slackPublicationState: result.slackPublication.enqueue?.kind === "enqueued" || result.slackPublication.enqueue?.kind === "replayed" ? result.slackPublication.enqueue.publication.state : null
21598
+ }
21599
+ })
21600
+ );
21601
+ }
21602
+ );
21178
21603
  }
21179
21604
  function registerFleetTools(server, deps, grant, sessionId, json) {
21180
21605
  const services = {
@@ -21668,7 +22093,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21668
22093
  includeLastMessage: z43.boolean().optional(),
21669
22094
  orderBy: z43.enum(["createdAt", "updatedAt", "relevance"]).optional(),
21670
22095
  updatedAfter: z43.string().max(64).optional(),
21671
- query: z43.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).optional(),
22096
+ query: z43.string().max(MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS).optional(),
21672
22097
  statuses: z43.array(
21673
22098
  z43.enum([
21674
22099
  "queued",
@@ -21920,7 +22345,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21920
22345
  server.registerTool(
21921
22346
  "session_wait",
21922
22347
  {
21923
- description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session); returns immediately when anything already changed. Only turn lifecycle, agent.message.completed, blocking failures, goal facts, and session status/control changes count as a change; raw deltas, tool receipts, and sandbox diagnostics never wake it. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
22348
+ description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the backward-compatible default and returns on turn lifecycle, agent.message.completed, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
21924
22349
  inputSchema: {
21925
22350
  targets: z43.array(
21926
22351
  z43.object({
@@ -21931,10 +22356,13 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21931
22356
  includeOwnPendingUpdates: z43.boolean().optional().describe(
21932
22357
  "Also return when your own session has pending machine input (default true)."
21933
22358
  ),
22359
+ waitFor: z43.enum(["change", "completion"]).optional().describe(
22360
+ "change (default) returns on relevant activity; completion ignores messages, goal/progress, maintenance, and continuation segments until a result-bearing final turn or blocker."
22361
+ ),
21934
22362
  maxWaitSeconds: z43.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional()
21935
22363
  }
21936
22364
  },
21937
- async ({ targets, includeOwnPendingUpdates, maxWaitSeconds }, extra) => {
22365
+ async ({ targets, includeOwnPendingUpdates, waitFor, maxWaitSeconds }, extra) => {
21938
22366
  const distinct = new Set(targets.map((target) => target.sessionId));
21939
22367
  if (distinct.size !== targets.length) {
21940
22368
  throw new Error("session_wait targets must name distinct sessions");
@@ -21944,6 +22372,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21944
22372
  await requireSession(deps.db, grant.workspaceId, target.sessionId);
21945
22373
  }
21946
22374
  const ownSessionId = includeOwnPendingUpdates === false ? null : callerSessionId;
22375
+ const targetEventTypes = waitFor === "completion" ? SESSION_WAIT_COMPLETION_EVENT_TYPES : SESSION_WAIT_EVENT_TYPES;
21947
22376
  const signal = extra?.signal;
21948
22377
  const workspaceId = grant.workspaceId;
21949
22378
  return json(
@@ -21951,6 +22380,8 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21951
22380
  targets,
21952
22381
  ownSessionId,
21953
22382
  maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1e3,
22383
+ targetEventTypes,
22384
+ targetEventMatches: waitFor === "completion" ? sessionWaitCompletionEventMatches : void 0,
21954
22385
  signal,
21955
22386
  source: {
21956
22387
  reauthorizeTargets: async (sessionIds) => {
@@ -21969,7 +22400,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21969
22400
  direction: "after",
21970
22401
  limit: SESSION_WAIT_EVENTS_PER_TARGET,
21971
22402
  payloadMode: "full",
21972
- includeTypes: SESSION_WAIT_EVENT_TYPES,
22403
+ includeTypes: targetEventTypes,
21973
22404
  maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4
21974
22405
  });
21975
22406
  return { events: page.events, hasMore: page.hasMore };
@@ -21989,6 +22420,9 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21989
22420
  if (can("sessions:create") && sessionCreateVisible) {
21990
22421
  const sessionCreateInput = z43.object({
21991
22422
  initialMessage: z43.string().min(1),
22423
+ title: z43.string().min(1).max(SESSION_TITLE_MAX_CHARACTERS).optional().describe(
22424
+ "Concise semantic title for the child session. Omit only when the delegated goal or initial message already provides a suitable title; OpenGeni derives a sensitive-safe bounded fallback from that text."
22425
+ ),
21992
22426
  instructions: z43.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
21993
22427
  goal: z43.unknown().optional(),
21994
22428
  resources: z43.array(z43.unknown()).optional(),
@@ -22047,7 +22481,7 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
22047
22481
  server.registerTool(
22048
22482
  "session_create",
22049
22483
  {
22050
- description: "Spawn a new agent session (a worker). The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
22484
+ description: "Spawn a new agent session (a worker) only for a concrete, bounded subtask that can run independently and has a defined integration point in your current work. Do not delegate work you will also perform yourself; track the child and join its actual result before completing dependent work. Give the child a concise semantic title; if omitted, OpenGeni derives one from its delegated goal or initial message. The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
22051
22485
  inputSchema: sessionCreateInput
22052
22486
  },
22053
22487
  async (args) => {
@@ -22060,14 +22494,21 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
22060
22494
  if (callerSessionId !== null) {
22061
22495
  await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
22062
22496
  }
22063
- const { machineTarget, ...request } = args;
22064
- const result = await createSessionForRequestWithOutcome(deps, grant, grant.workspaceId, {
22065
- ...request,
22066
- ...machineTarget ? {
22067
- targetSandboxId: machineTarget.targetSandboxId,
22068
- ...machineTarget.workingDir !== void 0 ? { workingDir: machineTarget.workingDir } : {}
22069
- } : {}
22070
- });
22497
+ const { machineTarget, title, ...request } = args;
22498
+ const result = await createSessionForRequestWithOutcome(
22499
+ deps,
22500
+ grant,
22501
+ grant.workspaceId,
22502
+ {
22503
+ ...request,
22504
+ ...machineTarget ? {
22505
+ targetSandboxId: machineTarget.targetSandboxId,
22506
+ ...machineTarget.workingDir !== void 0 ? { workingDir: machineTarget.workingDir } : {}
22507
+ } : {}
22508
+ },
22509
+ void 0,
22510
+ title === void 0 ? {} : { automaticTitleCandidate: title }
22511
+ );
22071
22512
  return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
22072
22513
  } catch (error) {
22073
22514
  return orchestrationFailureResult("session_create", error);
@@ -23821,7 +24262,7 @@ function registerCapabilityRoutes(app, deps) {
23821
24262
  });
23822
24263
  app.post("/v1/workspaces/:workspaceId/capabilities", async (c) => {
23823
24264
  const workspaceId = c.req.param("workspaceId");
23824
- const grant = await requireAccessGrant3(c, deps, workspaceId, "workspace:admin");
24265
+ const grant = await requireAccessGrant3(c, deps, workspaceId, "capabilities:manage");
23825
24266
  const payload = CreateCapabilityCatalogItemRequest.parse(await c.req.json());
23826
24267
  return c.json(
23827
24268
  await createCatalogItem({ db, accountId: grant.accountId, workspaceId, payload }),
@@ -23849,7 +24290,7 @@ function registerCapabilityRoutes(app, deps) {
23849
24290
  });
23850
24291
  app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/enable", async (c) => {
23851
24292
  const workspaceId = c.req.param("workspaceId");
23852
- const grant = await requireAccessGrant3(c, deps, workspaceId, "workspace:admin");
24293
+ const grant = await requireAccessGrant3(c, deps, workspaceId, "capabilities:manage");
23853
24294
  const payload = EnableCapabilityRequest.parse(await c.req.json());
23854
24295
  const installation = await enableCapability({
23855
24296
  db,
@@ -23864,7 +24305,7 @@ function registerCapabilityRoutes(app, deps) {
23864
24305
  });
23865
24306
  app.post("/v1/workspaces/:workspaceId/capabilities/:capabilityId/disable", async (c) => {
23866
24307
  const workspaceId = c.req.param("workspaceId");
23867
- const grant = await requireAccessGrant3(c, deps, workspaceId, "workspace:admin");
24308
+ const grant = await requireAccessGrant3(c, deps, workspaceId, "capabilities:manage");
23868
24309
  const installation = await disableCapability({
23869
24310
  db,
23870
24311
  accountId: grant.accountId,
@@ -23990,8 +24431,10 @@ import {
23990
24431
  designateCodexAppsCredential,
23991
24432
  disconnectAllCodexAccounts,
23992
24433
  disconnectCodexAccount,
24434
+ disconnectOrganizationCodexAccount,
23993
24435
  encryptEnvironmentValue as encryptEnvironmentValue6,
23994
24436
  ensureCodexRotationSettings,
24437
+ ensureOrganizationCodexRotationSettings,
23995
24438
  fetchCodexUsageForAccount,
23996
24439
  fetchCodexRateLimitResetCreditsForAccount,
23997
24440
  fenceCodexResetRedemptionSend,
@@ -23999,24 +24442,36 @@ import {
23999
24442
  getCodexCredentialStatus,
24000
24443
  getCodexAppsSettings,
24001
24444
  getCodexRotationSettings,
24445
+ getOrganizationCodexRotationSettings,
24446
+ getWorkspaceCodexSubscriptionSource,
24002
24447
  listPendingCodexCapacityWakeTargets,
24003
24448
  listCodexAccountStatuses,
24449
+ listOrganizationCodexAccountStatuses,
24004
24450
  listCodexResetRedemptionRecoveries,
24451
+ nestedPostgresSqlState,
24005
24452
  releaseCodexResetRedemptionClaim,
24006
24453
  updateCodexAllocatorEligibility,
24007
24454
  loadCodexCredentialForRun,
24008
24455
  renameCodexAccount,
24456
+ renameOrganizationCodexAccount,
24009
24457
  setActiveCodexCredential,
24458
+ setActiveOrganizationCodexCredential,
24010
24459
  setInitialActiveCodexCredential,
24460
+ setWorkspaceCodexSubscriptionMode,
24461
+ setWorkspaceCodexSubscriptionModeInTransaction,
24462
+ updateOrganizationCodexRotationSettings,
24011
24463
  updateCodexRotationSettings,
24464
+ upsertOrganizationCodexSubscriptionCredential,
24012
24465
  upsertCodexSubscriptionCredential,
24013
- withCodexCapacityMutation
24466
+ withCodexCapacityMutation,
24467
+ withSessionCodexCapacityMutation
24014
24468
  } from "@opengeni/db";
24015
24469
  import { createSignedState as createSignedState7, readSignedState as readSignedState6 } from "@opengeni/github";
24016
24470
  import {
24017
24471
  getManagedSession,
24018
24472
  hasPermission as hasPermission11,
24019
- requireAccessGrant as requireAccessGrant4
24473
+ requireAccessGrant as requireAccessGrant4,
24474
+ requireCanonicalLocalAccountAdministrator
24020
24475
  } from "@opengeni/core";
24021
24476
  import { HTTPException as HTTPException21 } from "hono/http-exception";
24022
24477
  import * as z9 from "zod/v4";
@@ -24085,6 +24540,7 @@ var CODEX_PROVIDER_LABEL = "Codex subscription \xB7 no credits";
24085
24540
  function codexAccountJson(row, options = {}) {
24086
24541
  return {
24087
24542
  id: row.id,
24543
+ source: row.source,
24088
24544
  chatgptAccountId: row.chatgptAccountId,
24089
24545
  label: row.label,
24090
24546
  email: row.accountEmail,
@@ -24113,7 +24569,7 @@ function codexAccountJson(row, options = {}) {
24113
24569
  // P3 rotation cooldown: when set and in the future, this account is cooling-down.
24114
24570
  exhaustedUntil: row.exhaustedUntil,
24115
24571
  appsDesignated: options.appsCredentialId === row.id,
24116
- canEnableApps: options.appsCredentialId === null && options.canManageApps === true && options.humanSubjectId !== null && options.humanSubjectId !== void 0 && row.connectedBySubjectId === options.humanSubjectId && row.status === "active"
24572
+ canEnableApps: row.source === "workspace" && options.appsCredentialId === null && options.canManageApps === true && options.humanSubjectId !== null && options.humanSubjectId !== void 0 && row.connectedBySubjectId === options.humanSubjectId && row.status === "active"
24117
24573
  };
24118
24574
  }
24119
24575
  function codexUsageJson(payload) {
@@ -24159,6 +24615,48 @@ async function managedCookieHuman(c, deps) {
24159
24615
  browserSessionHash: await hashCodexBrowserSession(session.session.id)
24160
24616
  };
24161
24617
  }
24618
+ async function requireOrganizationCodexHuman(c, deps, organizationId2) {
24619
+ const parsed = z9.string().uuid().safeParse(organizationId2);
24620
+ if (!parsed.success) throw new HTTPException21(422, { message: "invalid organization id" });
24621
+ let human = await managedCookieHuman(c, deps);
24622
+ if (!human && deps.settings.productAccessMode === "local") {
24623
+ const local = await requireCanonicalLocalAccountAdministrator(c, deps, organizationId2);
24624
+ human = {
24625
+ subjectId: local.subjectId,
24626
+ browserSessionHash: await hashCodexBrowserSession(`local:${local.subjectId}`)
24627
+ };
24628
+ }
24629
+ if (!human) {
24630
+ throw new HTTPException21(401, { message: "organization administrator session required" });
24631
+ }
24632
+ try {
24633
+ await getOrganizationCodexRotationSettings(deps.db, {
24634
+ organizationId: organizationId2,
24635
+ actorSubjectId: human.subjectId
24636
+ });
24637
+ } catch (error) {
24638
+ const state = nestedPostgresSqlState(error);
24639
+ if (state === "42501") {
24640
+ throw new HTTPException21(403, { message: "organization administration is not authorized" });
24641
+ }
24642
+ if (state === "P0002") {
24643
+ throw new HTTPException21(404, { message: "organization not found" });
24644
+ }
24645
+ throw error;
24646
+ }
24647
+ return human;
24648
+ }
24649
+ async function requireWorkspaceCodexManagementSource(deps, workspaceId) {
24650
+ const source = await getWorkspaceCodexSubscriptionSource(deps.db, workspaceId);
24651
+ if (source.effectiveSource === "organization") {
24652
+ throw new HTTPException21(409, {
24653
+ message: "this Codex subscription is managed in Organization settings"
24654
+ });
24655
+ }
24656
+ if (source.effectiveSource === "disabled") {
24657
+ throw new HTTPException21(409, { message: "Codex is disabled for this workspace" });
24658
+ }
24659
+ }
24162
24660
  function requireSameOriginBrowserMutation(c, deps) {
24163
24661
  const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
24164
24662
  if (contentType !== "application/json") {
@@ -24166,13 +24664,13 @@ function requireSameOriginBrowserMutation(c, deps) {
24166
24664
  message: "JSON browser request required"
24167
24665
  });
24168
24666
  }
24169
- if (!deps.settings.publicBaseUrl) {
24667
+ if (deps.settings.productAccessMode !== "local" && !deps.settings.publicBaseUrl) {
24170
24668
  throw new HTTPException21(503, {
24171
24669
  message: "managed browser origin is not configured"
24172
24670
  });
24173
24671
  }
24174
- const expectedOrigin = new URL(deps.settings.publicBaseUrl).origin;
24175
- if (c.req.header("origin") !== expectedOrigin) {
24672
+ const origin = c.req.header("origin");
24673
+ if (deps.settings.productAccessMode === "local" ? !localBrowserOriginMatchesRequest(c, origin) : origin !== new URL(deps.settings.publicBaseUrl).origin) {
24176
24674
  throw new HTTPException21(403, {
24177
24675
  message: "same-origin browser request required"
24178
24676
  });
@@ -24183,6 +24681,30 @@ function requireSameOriginBrowserMutation(c, deps) {
24183
24681
  });
24184
24682
  }
24185
24683
  }
24684
+ function localBrowserOriginMatchesRequest(c, value) {
24685
+ if (!value) return false;
24686
+ let origin;
24687
+ try {
24688
+ origin = new URL(value);
24689
+ } catch {
24690
+ return false;
24691
+ }
24692
+ if (origin.origin !== value || origin.origin === "null" || origin.protocol !== "http:" && origin.protocol !== "https:") {
24693
+ return false;
24694
+ }
24695
+ const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
24696
+ const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
24697
+ if (protocol !== "http:" && protocol !== "https:") return false;
24698
+ const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
24699
+ if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
24700
+ let request;
24701
+ try {
24702
+ request = new URL(`${protocol}//${forwardedHost}`);
24703
+ } catch {
24704
+ return false;
24705
+ }
24706
+ return origin.protocol === request.protocol && origin.hostname === request.hostname && (request.port === "" || origin.port === request.port);
24707
+ }
24186
24708
  async function requireRedemptionHuman(c, deps, workspaceId) {
24187
24709
  if (deps.settings.productAccessMode !== "managed") {
24188
24710
  throw new HTTPException21(403, {
@@ -24201,7 +24723,7 @@ async function requireRedemptionHuman(c, deps, workspaceId) {
24201
24723
  message: "managed browser session required"
24202
24724
  });
24203
24725
  }
24204
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
24726
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
24205
24727
  if (grant.subjectId !== human.subjectId) {
24206
24728
  throw new HTTPException21(403, {
24207
24729
  message: "managed browser identity mismatch"
@@ -24432,9 +24954,244 @@ async function signalPendingCodexCapacityTargets(deps, workspaceId) {
24432
24954
  var CODEX_DEVICE_EXPIRY_SECONDS = 15 * 60;
24433
24955
  function registerCodexRoutes(app, deps) {
24434
24956
  const { db, settings, githubStateSecret } = deps;
24957
+ app.get("/v1/workspaces/:workspaceId/codex/source", async (c) => {
24958
+ const workspaceId = c.req.param("workspaceId");
24959
+ await requireAccessGrant4(c, deps, workspaceId, "workspace:read");
24960
+ return c.json(await getWorkspaceCodexSubscriptionSource(db, workspaceId));
24961
+ });
24962
+ app.patch("/v1/workspaces/:workspaceId/codex/source", async (c) => {
24963
+ const workspaceId = c.req.param("workspaceId");
24964
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
24965
+ const parsed = z9.object({ mode: z9.enum(["automatic", "workspace", "organization", "disabled"]) }).safeParse(await c.req.json().catch(() => null));
24966
+ if (!parsed.success) {
24967
+ throw new HTTPException21(400, { message: "a valid Codex source mode is required" });
24968
+ }
24969
+ try {
24970
+ return c.json(
24971
+ await setWorkspaceCodexSubscriptionMode(db, {
24972
+ accountId: grant.accountId,
24973
+ workspaceId,
24974
+ subjectId: grant.subjectId,
24975
+ mode: parsed.data.mode
24976
+ })
24977
+ );
24978
+ } catch (error) {
24979
+ if (error instanceof Error && (error.message.includes("personal workspaces") || error.message.includes("active turns are using it"))) {
24980
+ throw new HTTPException21(409, { message: error.message });
24981
+ }
24982
+ throw error;
24983
+ }
24984
+ });
24985
+ app.get("/v1/organizations/:organizationId/codex/accounts", async (c) => {
24986
+ const organizationId2 = c.req.param("organizationId");
24987
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
24988
+ const [accounts, rotation] = await Promise.all([
24989
+ listOrganizationCodexAccountStatuses(db, {
24990
+ organizationId: organizationId2,
24991
+ actorSubjectId: human.subjectId
24992
+ }),
24993
+ getOrganizationCodexRotationSettings(db, {
24994
+ organizationId: organizationId2,
24995
+ actorSubjectId: human.subjectId
24996
+ })
24997
+ ]);
24998
+ return c.json({
24999
+ accounts: accounts.map((account) => codexAccountJson(account)),
25000
+ activeAccountId: rotation?.activeCredentialId ?? null,
25001
+ settings: {
25002
+ rotationEnabled: rotation?.rotationEnabled ?? false,
25003
+ rotationStrategy: "sharded",
25004
+ activeCredentialId: rotation?.activeCredentialId ?? null
25005
+ }
25006
+ });
25007
+ });
25008
+ app.post("/v1/organizations/:organizationId/codex/connect/start", async (c) => {
25009
+ const organizationId2 = c.req.param("organizationId");
25010
+ requireSameOriginBrowserMutation(c, deps);
25011
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25012
+ let start;
25013
+ try {
25014
+ start = await startDeviceCode();
25015
+ } catch (error) {
25016
+ throw new HTTPException21(502, {
25017
+ message: error instanceof CodexDeviceError ? error.message : "failed to start Codex device login"
25018
+ });
25019
+ }
25020
+ return c.json({
25021
+ userCode: start.userCode,
25022
+ verificationUri: start.verificationUri,
25023
+ intervalSeconds: start.intervalSeconds,
25024
+ state: createSignedState7(githubStateSecret, {
25025
+ organizationId: organizationId2,
25026
+ actorSubjectId: human.subjectId,
25027
+ deviceAuthId: start.deviceAuthId,
25028
+ userCode: start.userCode
25029
+ })
25030
+ });
25031
+ });
25032
+ app.post("/v1/organizations/:organizationId/codex/connect/poll", async (c) => {
25033
+ const organizationId2 = c.req.param("organizationId");
25034
+ requireSameOriginBrowserMutation(c, deps);
25035
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25036
+ const { state } = await c.req.json().catch(() => null);
25037
+ const payload = state ? readSignedState6(state, githubStateSecret) : null;
25038
+ if (!payload || payload.organizationId !== organizationId2 || payload.actorSubjectId !== human.subjectId || !payload.deviceAuthId || !payload.userCode) {
25039
+ throw new HTTPException21(400, { message: "codex connect state is invalid or expired" });
25040
+ }
25041
+ if (typeof payload.iat === "number" && Date.now() / 1e3 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS) {
25042
+ return c.json({ status: "expired" });
25043
+ }
25044
+ let poll;
25045
+ try {
25046
+ poll = await pollDeviceCode({
25047
+ deviceAuthId: payload.deviceAuthId,
25048
+ userCode: payload.userCode
25049
+ });
25050
+ } catch (error) {
25051
+ throw new HTTPException21(502, {
25052
+ message: error instanceof CodexDeviceError ? error.message : "codex device poll failed"
25053
+ });
25054
+ }
25055
+ if (poll.status === "pending") return c.json({ status: "pending" });
25056
+ if (poll.status === "expired") return c.json({ status: "expired" });
25057
+ let tokens;
25058
+ try {
25059
+ tokens = await exchangeDeviceCode({
25060
+ authorizationCode: poll.authorizationCode,
25061
+ codeVerifier: poll.codeVerifier
25062
+ });
25063
+ } catch (error) {
25064
+ throw new HTTPException21(502, {
25065
+ message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed"
25066
+ });
25067
+ }
25068
+ const key = environmentsEncryptionKeyBytes2(settings);
25069
+ if (!key) {
25070
+ throw new HTTPException21(500, {
25071
+ message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured"
25072
+ });
25073
+ }
25074
+ const id = parseIdToken(tokens.idToken);
25075
+ await ensureOrganizationCodexRotationSettings(db, {
25076
+ organizationId: organizationId2,
25077
+ actorSubjectId: human.subjectId
25078
+ });
25079
+ const upserted = await upsertOrganizationCodexSubscriptionCredential(db, {
25080
+ organizationId: organizationId2,
25081
+ actorSubjectId: human.subjectId,
25082
+ credentialEncrypted: encryptEnvironmentValue6(
25083
+ key,
25084
+ JSON.stringify({
25085
+ access_token: tokens.accessToken,
25086
+ refresh_token: tokens.refreshToken,
25087
+ id_token: tokens.idToken
25088
+ })
25089
+ ),
25090
+ chatgptAccountId: id.chatgptAccountId,
25091
+ scopes: null,
25092
+ planType: id.planType,
25093
+ isFedramp: id.isFedramp,
25094
+ expiresAt: accessTokenExpiry(tokens.accessToken),
25095
+ lastRefreshAt: /* @__PURE__ */ new Date(),
25096
+ accountEmail: id.email ?? null,
25097
+ label: id.email ?? id.chatgptAccountId ?? null
25098
+ });
25099
+ await signalCodexCapacityTargets(deps, upserted.wakeTargets);
25100
+ const rotation = await getOrganizationCodexRotationSettings(db, {
25101
+ organizationId: organizationId2,
25102
+ actorSubjectId: human.subjectId
25103
+ });
25104
+ return c.json({
25105
+ status: "connected",
25106
+ plan: id.planType,
25107
+ accountId: upserted.id,
25108
+ isActive: rotation?.activeCredentialId === upserted.id
25109
+ });
25110
+ });
25111
+ app.post("/v1/organizations/:organizationId/codex/accounts/:accountId/activate", async (c) => {
25112
+ const organizationId2 = c.req.param("organizationId");
25113
+ requireSameOriginBrowserMutation(c, deps);
25114
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25115
+ const credentialId = c.req.param("accountId");
25116
+ const activation = await setActiveOrganizationCodexCredential(db, {
25117
+ organizationId: organizationId2,
25118
+ actorSubjectId: human.subjectId,
25119
+ credentialId
25120
+ });
25121
+ if (!activation.activated) {
25122
+ throw new HTTPException21(404, { message: "codex account not found" });
25123
+ }
25124
+ await signalCodexCapacityTargets(deps, activation.wakeTargets);
25125
+ return c.json({ activated: true, accountId: credentialId });
25126
+ });
25127
+ app.patch("/v1/organizations/:organizationId/codex/settings", async (c) => {
25128
+ const organizationId2 = c.req.param("organizationId");
25129
+ requireSameOriginBrowserMutation(c, deps);
25130
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25131
+ const parsed = z9.object({ rotationEnabled: z9.boolean() }).safeParse(await c.req.json().catch(() => null));
25132
+ if (!parsed.success) {
25133
+ throw new HTTPException21(400, { message: "rotationEnabled is required" });
25134
+ }
25135
+ const updated = await updateOrganizationCodexRotationSettings(db, {
25136
+ organizationId: organizationId2,
25137
+ actorSubjectId: human.subjectId,
25138
+ rotationEnabled: parsed.data.rotationEnabled
25139
+ });
25140
+ if (!updated) throw new HTTPException21(404, { message: "Codex settings not found" });
25141
+ await signalCodexCapacityTargets(deps, updated.wakeTargets);
25142
+ return c.json({
25143
+ rotationEnabled: updated.rotationEnabled,
25144
+ rotationStrategy: "sharded",
25145
+ activeCredentialId: updated.activeCredentialId
25146
+ });
25147
+ });
25148
+ app.patch("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
25149
+ const organizationId2 = c.req.param("organizationId");
25150
+ requireSameOriginBrowserMutation(c, deps);
25151
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25152
+ const body4 = await c.req.json().catch(() => null);
25153
+ const renamed = await renameOrganizationCodexAccount(db, {
25154
+ organizationId: organizationId2,
25155
+ actorSubjectId: human.subjectId,
25156
+ credentialId: c.req.param("accountId"),
25157
+ label: typeof body4?.label === "string" ? body4.label : null
25158
+ });
25159
+ if (!renamed) throw new HTTPException21(404, { message: "codex account not found" });
25160
+ const accounts = await listOrganizationCodexAccountStatuses(db, {
25161
+ organizationId: organizationId2,
25162
+ actorSubjectId: human.subjectId
25163
+ });
25164
+ const row = accounts.find((account) => account.id === c.req.param("accountId"));
25165
+ if (!row) throw new HTTPException21(404, { message: "codex account not found" });
25166
+ return c.json(codexAccountJson(row));
25167
+ });
25168
+ app.delete("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
25169
+ const organizationId2 = c.req.param("organizationId");
25170
+ requireSameOriginBrowserMutation(c, deps);
25171
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId2);
25172
+ let result;
25173
+ try {
25174
+ result = await disconnectOrganizationCodexAccount(db, {
25175
+ organizationId: organizationId2,
25176
+ actorSubjectId: human.subjectId,
25177
+ credentialId: c.req.param("accountId")
25178
+ });
25179
+ } catch (error) {
25180
+ const cause = error?.cause;
25181
+ const message = cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
25182
+ if (message.includes("active turns are using it")) {
25183
+ throw new HTTPException21(409, {
25184
+ message: "Codex subscription cannot disconnect while active turns are using it"
25185
+ });
25186
+ }
25187
+ throw error;
25188
+ }
25189
+ await signalCodexCapacityTargets(deps, result.wakeTargets);
25190
+ return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
25191
+ });
24435
25192
  app.post("/v1/workspaces/:workspaceId/codex/connect/start", async (c) => {
24436
25193
  const workspaceId = c.req.param("workspaceId");
24437
- await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25194
+ await requireAccessGrant4(c, deps, workspaceId, "connections:write");
24438
25195
  let start;
24439
25196
  try {
24440
25197
  start = await startDeviceCode();
@@ -24457,7 +25214,7 @@ function registerCodexRoutes(app, deps) {
24457
25214
  });
24458
25215
  app.post("/v1/workspaces/:workspaceId/codex/connect/poll", async (c) => {
24459
25216
  const workspaceId = c.req.param("workspaceId");
24460
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25217
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
24461
25218
  const { state } = await c.req.json();
24462
25219
  const payload = state ? readSignedState6(state, githubStateSecret) : null;
24463
25220
  if (!payload || payload.workspaceId !== workspaceId || !payload.deviceAuthId || !payload.userCode) {
@@ -24504,47 +25261,56 @@ function registerCodexRoutes(app, deps) {
24504
25261
  message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured"
24505
25262
  });
24506
25263
  }
24507
- await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
24508
- const mutation = await withCodexCapacityMutation(
24509
- db,
24510
- { workspaceId, reason: "codex_credential_connected" },
24511
- async (tx) => {
24512
- const upserted2 = await upsertCodexSubscriptionCredential(tx, {
24513
- accountId: grant.accountId,
24514
- workspaceId,
24515
- credentialEncrypted: encryptEnvironmentValue6(
24516
- key,
24517
- JSON.stringify({
24518
- access_token: tokens.accessToken,
24519
- refresh_token: tokens.refreshToken,
24520
- id_token: tokens.idToken
24521
- })
24522
- ),
24523
- chatgptAccountId: id.chatgptAccountId,
24524
- scopes: null,
24525
- // device grant scopes are discovered at runtime, not asserted here
24526
- planType: id.planType,
24527
- isFedramp: id.isFedramp,
24528
- expiresAt: accessTokenExpiry(tokens.accessToken),
24529
- lastRefreshAt: /* @__PURE__ */ new Date(),
24530
- accountEmail: id.email ?? null,
24531
- label: id.email ?? id.chatgptAccountId ?? null,
24532
- connectedBySubjectId: connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null
24533
- });
24534
- return { result: upserted2, changed: upserted2.kind === "upserted" };
24535
- }
24536
- );
24537
- const upserted = mutation.result;
25264
+ const mutation = await withSessionCodexCapacityMutation(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
25265
+ const upserted2 = await upsertCodexSubscriptionCredential(tx, {
25266
+ accountId: grant.accountId,
25267
+ workspaceId,
25268
+ credentialEncrypted: encryptEnvironmentValue6(
25269
+ key,
25270
+ JSON.stringify({
25271
+ access_token: tokens.accessToken,
25272
+ refresh_token: tokens.refreshToken,
25273
+ id_token: tokens.idToken
25274
+ })
25275
+ ),
25276
+ chatgptAccountId: id.chatgptAccountId,
25277
+ scopes: null,
25278
+ // device grant scopes are discovered at runtime, not asserted here
25279
+ planType: id.planType,
25280
+ isFedramp: id.isFedramp,
25281
+ expiresAt: accessTokenExpiry(tokens.accessToken),
25282
+ lastRefreshAt: /* @__PURE__ */ new Date(),
25283
+ accountEmail: id.email ?? null,
25284
+ label: id.email ?? id.chatgptAccountId ?? null,
25285
+ connectedBySubjectId: connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null
25286
+ });
25287
+ if (upserted2.kind === "unresolved_redemption") {
25288
+ return { result: { upserted: upserted2, isActive: false }, changed: false };
25289
+ }
25290
+ await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
25291
+ await setInitialActiveCodexCredential(tx, workspaceId, upserted2.id);
25292
+ const source = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
25293
+ await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
25294
+ accountId: grant.accountId,
25295
+ workspaceId,
25296
+ subjectId: grant.subjectId,
25297
+ mode: source.workspaceKind === "personal" ? "automatic" : "workspace"
25298
+ });
25299
+ const rotation = await getCodexRotationSettings(tx, workspaceId);
25300
+ return {
25301
+ result: {
25302
+ upserted: upserted2,
25303
+ isActive: rotation?.activeCredentialId === upserted2.id
25304
+ },
25305
+ changed: true
25306
+ };
25307
+ });
25308
+ const { upserted, isActive } = mutation.result;
24538
25309
  if (upserted.kind === "unresolved_redemption") {
24539
25310
  throw new HTTPException21(409, {
24540
25311
  message: "this subscription has an unresolved reset redemption; recover it before changing ownership"
24541
25312
  });
24542
25313
  }
24543
- const rotation = await getCodexRotationSettings(db, workspaceId);
24544
- let isActive = rotation?.activeCredentialId === upserted.id;
24545
- if (!isActive && rotation?.activeCredentialId == null) {
24546
- isActive = await setInitialActiveCodexCredential(db, workspaceId, upserted.id);
24547
- }
24548
25314
  await signalCodexCapacityTargets(deps, mutation.wakeTargets);
24549
25315
  return c.json({ status: "connected", plan: id.planType, accountId: upserted.id, isActive });
24550
25316
  });
@@ -24555,7 +25321,10 @@ function registerCodexRoutes(app, deps) {
24555
25321
  if (!status) {
24556
25322
  return c.json({ connected: false });
24557
25323
  }
24558
- const accounts = await listCodexAccountStatuses(db, workspaceId);
25324
+ const [accounts, source] = await Promise.all([
25325
+ listCodexAccountStatuses(db, workspaceId),
25326
+ getWorkspaceCodexSubscriptionSource(db, workspaceId)
25327
+ ]);
24559
25328
  const activeRow = accounts.find((account) => account.id === status.credentialId) ?? null;
24560
25329
  const activeAccount = activeRow ? {
24561
25330
  id: activeRow.id,
@@ -24595,17 +25364,19 @@ function registerCodexRoutes(app, deps) {
24595
25364
  // ClientModel[] the picker surfaces under the "no credits" group
24596
25365
  activeAccount,
24597
25366
  // the account a session runs on when unpinned (label for the indicator)
24598
- accountCount: accounts.length
25367
+ accountCount: accounts.length,
25368
+ source
24599
25369
  });
24600
25370
  });
24601
25371
  app.get("/v1/workspaces/:workspaceId/codex/accounts", async (c) => {
24602
25372
  const workspaceId = c.req.param("workspaceId");
24603
25373
  const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:read");
24604
- const [accounts, rotation, apps, human] = await Promise.all([
25374
+ const [accounts, rotation, apps, human, source] = await Promise.all([
24605
25375
  listCodexAccountStatuses(db, workspaceId),
24606
25376
  getCodexRotationSettings(db, workspaceId),
24607
25377
  getCodexAppsSettings(db, workspaceId),
24608
- managedCookieHuman(c, deps)
25378
+ managedCookieHuman(c, deps),
25379
+ getWorkspaceCodexSubscriptionSource(db, workspaceId)
24609
25380
  ]);
24610
25381
  const activeAccountId = rotation?.activeCredentialId ?? null;
24611
25382
  const humanSubjectId = human?.subjectId === grant.subjectId ? human.subjectId : null;
@@ -24619,6 +25390,7 @@ function registerCodexRoutes(app, deps) {
24619
25390
  })
24620
25391
  ),
24621
25392
  activeAccountId,
25393
+ source,
24622
25394
  apps: {
24623
25395
  available: settings.codexConnectedAppsEnabled,
24624
25396
  credentialId: apps.credentialId,
@@ -24637,6 +25409,7 @@ function registerCodexRoutes(app, deps) {
24637
25409
  });
24638
25410
  app.post("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
24639
25411
  const workspaceId = c.req.param("workspaceId");
25412
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24640
25413
  if (!settings.codexConnectedAppsEnabled) {
24641
25414
  throw new HTTPException21(409, { message: "Codex Apps is disabled for this deployment" });
24642
25415
  }
@@ -24679,6 +25452,7 @@ function registerCodexRoutes(app, deps) {
24679
25452
  });
24680
25453
  app.delete("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
24681
25454
  const workspaceId = c.req.param("workspaceId");
25455
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24682
25456
  const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
24683
25457
  const parsed = z9.object({ expectedVersion: z9.number().int().nonnegative() }).safeParse(await c.req.json().catch(() => null));
24684
25458
  if (!parsed.success) {
@@ -24703,7 +25477,8 @@ function registerCodexRoutes(app, deps) {
24703
25477
  });
24704
25478
  app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
24705
25479
  const workspaceId = c.req.param("workspaceId");
24706
- await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25480
+ await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25481
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24707
25482
  const accountId = c.req.param("accountId");
24708
25483
  const mutation = await withCodexCapacityMutation(
24709
25484
  db,
@@ -24722,7 +25497,8 @@ function registerCodexRoutes(app, deps) {
24722
25497
  });
24723
25498
  app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
24724
25499
  const workspaceId = c.req.param("workspaceId");
24725
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25500
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25501
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24726
25502
  const body4 = await c.req.json().catch(() => ({}));
24727
25503
  const patch = {};
24728
25504
  if (typeof body4.rotationEnabled === "boolean") {
@@ -24759,7 +25535,8 @@ function registerCodexRoutes(app, deps) {
24759
25535
  });
24760
25536
  app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
24761
25537
  const workspaceId = c.req.param("workspaceId");
24762
- await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25538
+ await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25539
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24763
25540
  const accountId = c.req.param("accountId");
24764
25541
  const body4 = await c.req.json();
24765
25542
  const label = typeof body4.label === "string" ? body4.label : null;
@@ -24776,7 +25553,8 @@ function registerCodexRoutes(app, deps) {
24776
25553
  });
24777
25554
  app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/allocator", async (c) => {
24778
25555
  const workspaceId = c.req.param("workspaceId");
24779
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25556
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25557
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24780
25558
  const parsed = z9.object({
24781
25559
  enabled: z9.boolean(),
24782
25560
  expectedVersion: z9.number().int().positive()
@@ -24809,7 +25587,8 @@ function registerCodexRoutes(app, deps) {
24809
25587
  });
24810
25588
  app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
24811
25589
  const workspaceId = c.req.param("workspaceId");
24812
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25590
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25591
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24813
25592
  const accountId = c.req.param("accountId");
24814
25593
  const mutation = await withCodexCapacityMutation(
24815
25594
  db,
@@ -24830,7 +25609,8 @@ function registerCodexRoutes(app, deps) {
24830
25609
  });
24831
25610
  app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
24832
25611
  const workspaceId = c.req.param("workspaceId");
24833
- const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:admin");
25612
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "connections:write");
25613
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
24834
25614
  const mutation = await withCodexCapacityMutation(
24835
25615
  db,
24836
25616
  { workspaceId, reason: "codex_credentials_disconnected" },
@@ -24913,7 +25693,7 @@ function registerCodexRoutes(app, deps) {
24913
25693
  const grant = await requireAccessGrant4(c, deps, workspaceId, "workspace:read");
24914
25694
  const human = await managedCookieHuman(c, deps);
24915
25695
  const accounts = await listCodexAccountStatuses(db, workspaceId);
24916
- const ownerRecoveries = human && human.subjectId === grant.subjectId && hasPermission11(grant.permissions, "workspace:admin") ? await listCodexResetRedemptionRecoveries(db, {
25696
+ const ownerRecoveries = human && human.subjectId === grant.subjectId && hasPermission11(grant.permissions, "connections:write") ? await listCodexResetRedemptionRecoveries(db, {
24917
25697
  accountId: grant.accountId,
24918
25698
  workspaceId,
24919
25699
  subjectId: human.subjectId
@@ -24928,14 +25708,17 @@ function registerCodexRoutes(app, deps) {
24928
25708
  const account = queue.shift();
24929
25709
  if (!account) return;
24930
25710
  const canResumeRedemption = Boolean(
24931
- human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission11(grant.permissions, "workspace:admin")
25711
+ account.source === "workspace" && human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission11(grant.permissions, "connections:write")
24932
25712
  );
24933
25713
  const canRedeem = canResumeRedemption && account.status === "active";
24934
- const redemptionAccess = codexRedemptionAccess({
25714
+ const redemptionAccess = account.source === "organization" ? {
25715
+ ownership: "managed_human_unavailable",
25716
+ canClaimUnownedViaReconnect: false
25717
+ } : codexRedemptionAccess({
24935
25718
  connectedBySubjectId: account.connectedBySubjectId,
24936
25719
  grantSubjectId: grant.subjectId,
24937
25720
  managedHumanSubjectId: human?.subjectId ?? null,
24938
- canManage: hasPermission11(grant.permissions, "workspace:admin")
25721
+ canManage: hasPermission11(grant.permissions, "connections:write")
24939
25722
  });
24940
25723
  overview[account.id] = await fetchCodexAccountOverview(
24941
25724
  deps,
@@ -24971,17 +25754,20 @@ function registerCodexRoutes(app, deps) {
24971
25754
  await Promise.all(
24972
25755
  accounts.filter((account) => overview[account.id] == null).map(async (account) => {
24973
25756
  const canResumeRedemption = Boolean(
24974
- human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission11(grant.permissions, "workspace:admin")
25757
+ account.source === "workspace" && human && human.subjectId === grant.subjectId && human.subjectId === account.connectedBySubjectId && hasPermission11(grant.permissions, "connections:write")
24975
25758
  );
24976
25759
  const fallback = await fetchCodexAccountOverview(
24977
25760
  deps,
24978
25761
  workspaceId,
24979
25762
  account,
24980
- codexRedemptionAccess({
25763
+ account.source === "organization" ? {
25764
+ ownership: "managed_human_unavailable",
25765
+ canClaimUnownedViaReconnect: false
25766
+ } : codexRedemptionAccess({
24981
25767
  connectedBySubjectId: account.connectedBySubjectId,
24982
25768
  grantSubjectId: grant.subjectId,
24983
25769
  managedHumanSubjectId: human?.subjectId ?? null,
24984
- canManage: hasPermission11(grant.permissions, "workspace:admin")
25770
+ canManage: hasPermission11(grant.permissions, "connections:write")
24985
25771
  }),
24986
25772
  false,
24987
25773
  canResumeRedemption,
@@ -25012,6 +25798,11 @@ function registerCodexRoutes(app, deps) {
25012
25798
  const accounts = await listCodexAccountStatuses(db, workspaceId);
25013
25799
  const account = accounts.find((candidate) => candidate.id === credentialId);
25014
25800
  if (!account) throw new HTTPException21(404, { message: "codex account not found" });
25801
+ if (account.source === "organization") {
25802
+ throw new HTTPException21(409, {
25803
+ message: "organization Codex subscriptions are managed in Organization settings"
25804
+ });
25805
+ }
25015
25806
  let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
25016
25807
  if (account.connectedBySubjectId !== human.subjectId) {
25017
25808
  throw new HTTPException21(403, {
@@ -26259,7 +27050,8 @@ import {
26259
27050
  getConnectionMetadata as getConnectionMetadata5,
26260
27051
  getWorkspaceGrant as getWorkspaceGrant6,
26261
27052
  loadConnectionCredentialForBroker as loadConnectionCredentialForBroker4,
26262
- persistProviderOAuthConnection
27053
+ persistProviderOAuthConnection,
27054
+ resolveNamedManagedPersonalWorkspaceGrant as resolveNamedManagedPersonalWorkspaceGrant2
26263
27055
  } from "@opengeni/db";
26264
27056
  import { createSignedState as createSignedState9, readSignedState as readSignedState8 } from "@opengeni/github";
26265
27057
  import {
@@ -26742,7 +27534,8 @@ async function providerFetch3(deps, url, init) {
26742
27534
  }
26743
27535
  }
26744
27536
  async function requireProviderOAuthGrant(deps, state) {
26745
- const grant = await getWorkspaceGrant6(deps.db, state.subjectId, state.workspaceId);
27537
+ const membershipGrant = await getWorkspaceGrant6(deps.db, state.subjectId, state.workspaceId);
27538
+ const grant = membershipGrant?.accountId === state.accountId ? membershipGrant : state.personalOwnerVerified ? await resolveNamedManagedPersonalWorkspaceGrant2(deps.db, state) : null;
26746
27539
  if (!grant || grant.accountId !== state.accountId || !hasPermission12(grant.permissions, "connections:write")) {
26747
27540
  throw new ProviderOAuthCallbackError("connection_conflict");
26748
27541
  }
@@ -28477,7 +29270,7 @@ import {
28477
29270
  requireAccessGrant as requireAccessGrant7,
28478
29271
  requireAccountAdminAuthorizationStamp,
28479
29272
  requireAccessGrantAuthorization as requireAccessGrantAuthorization2,
28480
- saveWorkspaceMemoryWithSlackPublication
29273
+ saveWorkspaceMemoryWithSlackPublication as saveWorkspaceMemoryWithSlackPublication2
28481
29274
  } from "@opengeni/core";
28482
29275
  import { recordWorkspaceUsage as recordWorkspaceUsage4, requireLimit as requireLimit4 } from "@opengeni/core";
28483
29276
 
@@ -29605,7 +30398,7 @@ function registerDocumentRoutes(app, deps) {
29605
30398
  }
29606
30399
  if (payload.status === "active") {
29607
30400
  try {
29608
- const result = await saveWorkspaceMemoryWithSlackPublication(
30401
+ const result = await saveWorkspaceMemoryWithSlackPublication2(
29609
30402
  db,
29610
30403
  {
29611
30404
  accountId: grant.accountId,
@@ -30472,6 +31265,7 @@ import {
30472
31265
  readMachineMetricsLatestForWorkspace
30473
31266
  } from "@opengeni/db";
30474
31267
  import { MachineView, MetricSample } from "@opengeni/contracts";
31268
+ import { managedSessionGroupBackend as managedSessionGroupBackend3 } from "@opengeni/core";
30475
31269
  import { selfhostedHeartbeatLiveness } from "@opengeni/runtime/sandbox";
30476
31270
  var ACTIVE_UPDATE_STATUSES = /* @__PURE__ */ new Set([
30477
31271
  "requested",
@@ -30603,7 +31397,8 @@ async function listMachines(services, input) {
30603
31397
  }
30604
31398
  }
30605
31399
  const machines = [];
30606
- if (session && session.sandboxBackend !== "none") {
31400
+ const groupBackend = session ? managedSessionGroupBackend3(services.settings.sandboxBackend, session.sandboxBackend) : null;
31401
+ if (session && groupBackend) {
30607
31402
  const groupActive = activeSandboxId === null;
30608
31403
  const groupLease = await readLease3(db, workspaceId, session.sandboxGroupId);
30609
31404
  machines.push(
@@ -30611,7 +31406,7 @@ async function listMachines(services, input) {
30611
31406
  sandboxId: session.sandboxGroupId,
30612
31407
  enrollmentId: null,
30613
31408
  name: "session sandbox",
30614
- kind: session.sandboxBackend === "selfhosted" ? "selfhosted" : "modal",
31409
+ kind: groupBackend === "opensandbox" ? "opensandbox" : "modal",
30615
31410
  state: "online",
30616
31411
  active: groupActive,
30617
31412
  isSessionGroup: true,
@@ -31568,12 +32363,37 @@ function trimmedVariableSetName(name) {
31568
32363
  }
31569
32364
 
31570
32365
  // src/routes/api-keys.ts
31571
- import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
31572
- import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
32366
+ import {
32367
+ CreateApiKeyRequest,
32368
+ CreateApiKeyResponse,
32369
+ CreateOrganizationApiKeyRequest
32370
+ } from "@opengeni/contracts";
32371
+ import {
32372
+ createApiKey,
32373
+ createOrganizationApiKey as createOrganizationApiKeyRecord,
32374
+ listApiKeys,
32375
+ listOrganizationApiKeys,
32376
+ OrganizationApiKeyLimitExceededError,
32377
+ revokeApiKey,
32378
+ revokeOrganizationApiKey
32379
+ } from "@opengeni/db";
32380
+ import { configuredStaticUsageLimits } from "@opengeni/config";
31573
32381
  import { zValidator } from "@hono/zod-validator";
31574
32382
  import { HTTPException as HTTPException31 } from "hono/http-exception";
31575
- import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
32383
+ import {
32384
+ accountScopedApiKeyWorkspaceAuthority,
32385
+ requireAccessContext,
32386
+ requireAccessGrant as requireAccessGrant12,
32387
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization6
32388
+ } from "@opengeni/core";
31576
32389
  import { requireLimit as requireLimit5 } from "@opengeni/core";
32390
+ var organizationApiKeyPermissions = [
32391
+ "account:read",
32392
+ "workspace:create",
32393
+ "workspace:read",
32394
+ "workspace:admin",
32395
+ "api_keys:manage"
32396
+ ];
31577
32397
  function registerApiKeyRoutes(app, deps) {
31578
32398
  app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
31579
32399
  const workspaceId = c.req.param("workspaceId");
@@ -31585,10 +32405,16 @@ function registerApiKeyRoutes(app, deps) {
31585
32405
  zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })),
31586
32406
  async (c) => {
31587
32407
  const workspaceId = c.req.param("workspaceId");
31588
- const grant = await requireAccessGrant12(c, deps, workspaceId, "api_keys:manage");
32408
+ const authorization = await requireAccessGrantAuthorization6(
32409
+ c,
32410
+ deps,
32411
+ workspaceId,
32412
+ "api_keys:manage"
32413
+ );
32414
+ const grant = authorization.grant;
31589
32415
  const body4 = c.req.valid("json");
31590
32416
  const permissions = body4.permissions.length > 0 ? body4.permissions : ["workspace:read"];
31591
- ensureDelegablePermissions(grant.permissions, permissions);
32417
+ ensureDelegablePermissions(authorization, permissions);
31592
32418
  await requireLimit5(deps, {
31593
32419
  accountId: grant.accountId,
31594
32420
  workspaceId,
@@ -31615,11 +32441,72 @@ function registerApiKeyRoutes(app, deps) {
31615
32441
  await requireAccessGrant12(c, deps, workspaceId, "api_keys:manage");
31616
32442
  return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
31617
32443
  });
32444
+ app.get("/v1/organizations/:organizationId/api-keys", async (c) => {
32445
+ const organizationId2 = c.req.param("organizationId");
32446
+ const context = await requireAccessContext(c, deps);
32447
+ requireOrganizationApiKeyControlPermission(context, organizationId2);
32448
+ return c.json({ apiKeys: await listOrganizationApiKeys(deps.db, organizationId2) });
32449
+ });
32450
+ app.post(
32451
+ "/v1/organizations/:organizationId/api-keys",
32452
+ zValidator("json", CreateOrganizationApiKeyRequest),
32453
+ async (c) => {
32454
+ const organizationId2 = c.req.param("organizationId");
32455
+ const context = await requireAccessContext(c, deps);
32456
+ requireOrganizationApiKeyControlPermission(context, organizationId2);
32457
+ const body4 = c.req.valid("json");
32458
+ const token = generateApiKeyToken();
32459
+ try {
32460
+ const apiKey = await createOrganizationApiKeyRecord(deps.db, {
32461
+ accountId: organizationId2,
32462
+ name: body4.name,
32463
+ description: body4.description ?? null,
32464
+ prefix: token.slice(0, 14),
32465
+ keyHash: await sha256Hex2(token),
32466
+ permissions: organizationApiKeyPermissions,
32467
+ expiresAt: body4.expiresAt ? new Date(body4.expiresAt) : null,
32468
+ maxActiveKeys: organizationApiKeyLimit(deps),
32469
+ rotationSourceApiKeyId: authenticatedApiKeyId(context)
32470
+ });
32471
+ return c.json(CreateApiKeyResponse.parse({ apiKey, token }), 201);
32472
+ } catch (error) {
32473
+ if (error instanceof OrganizationApiKeyLimitExceededError) {
32474
+ throw new HTTPException31(429, { message: error.message });
32475
+ }
32476
+ throw error;
32477
+ }
32478
+ }
32479
+ );
32480
+ app.delete("/v1/organizations/:organizationId/api-keys/:apiKeyId", async (c) => {
32481
+ const organizationId2 = c.req.param("organizationId");
32482
+ const context = await requireAccessContext(c, deps);
32483
+ requireOrganizationApiKeyControlPermission(context, organizationId2);
32484
+ const apiKey = await revokeOrganizationApiKey(deps.db, organizationId2, c.req.param("apiKeyId"));
32485
+ if (!apiKey) {
32486
+ throw new HTTPException31(404, { message: "API key not found" });
32487
+ }
32488
+ return c.json(apiKey);
32489
+ });
31618
32490
  }
31619
- function ensureDelegablePermissions(grantPermissions, requested) {
32491
+ function requireAccountPermission(context, accountId, permission) {
32492
+ const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
32493
+ if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
32494
+ throw new HTTPException31(403, { message: `missing permission: ${permission}` });
32495
+ }
32496
+ }
32497
+ function ensureDelegablePermissions(authorization, requested) {
32498
+ const grantPermissions = authorization.grant.permissions;
31620
32499
  if (grantPermissions.includes("workspace:admin")) {
32500
+ const accountLiteralPermissions = /* @__PURE__ */ new Set([
32501
+ "account:read",
32502
+ "account:admin",
32503
+ "workspace:create",
32504
+ "billing:read",
32505
+ "billing:manage"
32506
+ ]);
32507
+ const workspaceLiteralPermissions = /* @__PURE__ */ new Set(["members:manage", "secrets:read"]);
31621
32508
  const highTrustMissing = requested.filter(
31622
- (permission) => permission === "secrets:read" && !grantPermissions.includes("secrets:read")
32509
+ (permission) => accountLiteralPermissions.has(permission) && !authorization.accountGrant?.permissions.includes(permission) || workspaceLiteralPermissions.has(permission) && !grantPermissions.includes(permission)
31623
32510
  );
31624
32511
  if (highTrustMissing.length === 0) return;
31625
32512
  throw new HTTPException31(403, {
@@ -31633,6 +32520,23 @@ function ensureDelegablePermissions(grantPermissions, requested) {
31633
32520
  });
31634
32521
  }
31635
32522
  }
32523
+ function requireOrganizationApiKeyControlPermission(context, organizationId2) {
32524
+ requireAccountPermission(context, organizationId2, "api_keys:manage");
32525
+ if (!context.subjectId.startsWith("api_key:")) return;
32526
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
32527
+ if (!authority || authority.accountId !== organizationId2 || !authority.permissions.includes("api_keys:manage")) {
32528
+ throw new HTTPException31(403, { message: "organization API key authority required" });
32529
+ }
32530
+ }
32531
+ function authenticatedApiKeyId(context) {
32532
+ return context.subjectId.startsWith("api_key:") ? context.subjectId.slice("api_key:".length) : null;
32533
+ }
32534
+ function organizationApiKeyLimit(deps) {
32535
+ if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
32536
+ return null;
32537
+ }
32538
+ return configuredStaticUsageLimits(deps.settings).maxApiKeysPerWorkspace ?? null;
32539
+ }
31636
32540
  function generateApiKeyToken() {
31637
32541
  const bytes = new Uint8Array(32);
31638
32542
  crypto.getRandomValues(bytes);
@@ -31666,10 +32570,10 @@ import {
31666
32570
  } from "@opengeni/db";
31667
32571
  import { HTTPException as HTTPException32 } from "hono/http-exception";
31668
32572
  import Stripe from "stripe";
31669
- import { requireAccessContext } from "@opengeni/core";
32573
+ import { requireAccessContext as requireAccessContext2 } from "@opengeni/core";
31670
32574
  function registerBillingRoutes(app, deps) {
31671
32575
  app.get("/v1/billing", async (c) => {
31672
- const context = await requireAccessContext(c, deps);
32576
+ const context = await requireAccessContext2(c, deps);
31673
32577
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
31674
32578
  return c.json({
31675
32579
  mode: deps.settings.billingMode,
@@ -31677,7 +32581,7 @@ function registerBillingRoutes(app, deps) {
31677
32581
  });
31678
32582
  });
31679
32583
  app.get("/v1/billing/usage", async (c) => {
31680
- const context = await requireAccessContext(c, deps);
32584
+ const context = await requireAccessContext2(c, deps);
31681
32585
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
31682
32586
  const workspaceId = c.req.query("workspaceId");
31683
32587
  if (workspaceId && !context.workspaceGrants.some(
@@ -31695,7 +32599,7 @@ function registerBillingRoutes(app, deps) {
31695
32599
  });
31696
32600
  });
31697
32601
  app.get("/v1/billing/entitlements", async (c) => {
31698
- const context = await requireAccessContext(c, deps);
32602
+ const context = await requireAccessContext2(c, deps);
31699
32603
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
31700
32604
  return c.json({
31701
32605
  accountId,
@@ -31707,7 +32611,7 @@ function registerBillingRoutes(app, deps) {
31707
32611
  if (deps.settings.billingMode !== "stripe") {
31708
32612
  throw new HTTPException32(404, { message: "stripe billing is not enabled" });
31709
32613
  }
31710
- const context = await requireAccessContext(c, deps);
32614
+ const context = await requireAccessContext2(c, deps);
31711
32615
  const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
31712
32616
  if (!parsed.success) {
31713
32617
  throw new HTTPException32(400, {
@@ -31749,7 +32653,7 @@ function registerBillingRoutes(app, deps) {
31749
32653
  if (deps.settings.billingMode !== "stripe") {
31750
32654
  throw new HTTPException32(404, { message: "stripe billing is not enabled" });
31751
32655
  }
31752
- const context = await requireAccessContext(c, deps);
32656
+ const context = await requireAccessContext2(c, deps);
31753
32657
  const parsed = CreateBillingPortalRequest.safeParse(await c.req.json());
31754
32658
  if (!parsed.success) {
31755
32659
  throw new HTTPException32(400, {
@@ -33176,7 +34080,7 @@ function safeControlRequestId(value) {
33176
34080
 
33177
34081
  // src/interaction-frame-proxy.ts
33178
34082
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHmac as createHmac7, randomBytes as randomBytes8 } from "crypto";
33179
- import { canonicalPublicOrigin } from "@opengeni/config";
34083
+ import { canonicalPublicOrigin as canonicalPublicOrigin2 } from "@opengeni/config";
33180
34084
  import { WebSocket as UpstreamWebSocket } from "ws";
33181
34085
  var INTERACTION_FRAME_PROXY_PATH = "/v1/interaction/frame-proxy";
33182
34086
  var INTERACTION_FRAME_PROXY_PROTOCOL_PREFIX = "opengeni-frame-proxy.";
@@ -33196,7 +34100,7 @@ function placementUsesInteractionFrameProxy(backend, options) {
33196
34100
  return options?.openSandboxSignedEndpoints !== true;
33197
34101
  }
33198
34102
  function resolveInteractionFrameProxyRequestUrl(input) {
33199
- const publicOrigin = canonicalPublicOrigin(input.publicBaseUrl) ?? (input.webBaseUrl?.startsWith("https://") ? canonicalPublicOrigin(input.webBaseUrl) : null);
34103
+ const publicOrigin = canonicalPublicOrigin2(input.publicBaseUrl) ?? (input.webBaseUrl?.startsWith("https://") ? canonicalPublicOrigin2(input.webBaseUrl) : null);
33200
34104
  if (publicOrigin) return `${publicOrigin}/`;
33201
34105
  const request = new URL(input.requestUrl);
33202
34106
  const forwardedProto = firstForwardedValue(input.forwardedProto)?.toLowerCase();
@@ -33497,6 +34401,13 @@ var STALE_INTERACTION_ERROR_CODES = /* @__PURE__ */ new Set([
33497
34401
  "frame_stale",
33498
34402
  "attempt_stale"
33499
34403
  ]);
34404
+ var COMPUTER_FRAME_EVIDENCE_MISMATCH_REASONS = /* @__PURE__ */ new Set([
34405
+ "frame_session_mismatch",
34406
+ "frame_target_mismatch",
34407
+ "frame_controller_mismatch",
34408
+ "frame_media_mismatch",
34409
+ "frame_digest_mismatch"
34410
+ ]);
33500
34411
  function observeBrowserActionResult(observability, startedAtMs, request, receipt) {
33501
34412
  interactionOperationMetricObserver(observability)({
33502
34413
  resource: "browser",
@@ -33515,6 +34426,21 @@ function observeComputerActionResult(observability, startedAtMs, request, receip
33515
34426
  durationMs: elapsedMs(startedAtMs)
33516
34427
  });
33517
34428
  }
34429
+ function observeComputerFrameEvidenceMismatch(observability, reason2) {
34430
+ if (!observability || !COMPUTER_FRAME_EVIDENCE_MISMATCH_REASONS.has(reason2)) return;
34431
+ try {
34432
+ observability.incrementCounter({
34433
+ name: "opengeni_computer_frame_evidence_mismatches_total",
34434
+ help: "Computer frame evidence rejected at the controller-to-API boundary by bounded reason.",
34435
+ labels: { reason: reason2 }
34436
+ });
34437
+ } catch {
34438
+ }
34439
+ try {
34440
+ observability.warn("Computer frame evidence mismatch", { reason: reason2 });
34441
+ } catch {
34442
+ }
34443
+ }
33518
34444
  function observeLifecycleResult(observability, startedAtMs, response) {
33519
34445
  interactionOperationMetricObserver(observability)({
33520
34446
  resource: response.operation.resourceKind === "browser_session" ? "browser" : "computer",
@@ -36476,14 +37402,19 @@ function assertCreateReplay(request, session) {
36476
37402
  }
36477
37403
  }
36478
37404
  async function ensureHeadedBrowserDisplayStack(session, headless, linkedComputer) {
36479
- if (!browserNeedsStandaloneDisplayStack({ headless, linkedComputer })) return;
37405
+ if (!browserNeedsStandaloneDisplayStack({
37406
+ headless,
37407
+ linkedComputer,
37408
+ nativeBrowserControl: typeof session.ensureBrowserControl === "function"
37409
+ }))
37410
+ return;
36480
37411
  if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
36481
37412
  await ensureDisplayStack2(session, {
36482
37413
  telemetryContext: { callerKind: "viewer" }
36483
37414
  });
36484
37415
  }
36485
37416
  function browserNeedsStandaloneDisplayStack(input) {
36486
- return !input.headless && !input.linkedComputer;
37417
+ return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
36487
37418
  }
36488
37419
  async function ensureLinkedComputerController(deps, grant, browser, placement, client) {
36489
37420
  if (!browser.linkedComputerSessionId) return null;
@@ -37477,6 +38408,7 @@ import {
37477
38408
  BrowserControlServerUnsupportedError as BrowserControlServerUnsupportedError2,
37478
38409
  BrowserControlTransportError as BrowserControlTransportError4,
37479
38410
  BrowserControlUnsupportedError as BrowserControlUnsupportedError2,
38411
+ ComputerFrameEvidenceMismatchError,
37480
38412
  buildSelfhostedBackendSession as buildSelfhostedBackendSession3,
37481
38413
  buildStreamUrl as buildStreamUrl2,
37482
38414
  exposedPortEndpointFromUrl as exposedPortEndpointFromUrl3,
@@ -37484,6 +38416,7 @@ import {
37484
38416
  NatsControlRpc as NatsControlRpc5,
37485
38417
  NatsOpStreamTransport as NatsOpStreamTransport3,
37486
38418
  provisionBrowserControlClient as provisionBrowserControlClient2,
38419
+ validateComputerControlFrameEvidence,
37487
38420
  renewSandboxProviderExpiration as renewSandboxProviderExpiration3
37488
38421
  } from "@opengeni/runtime/sandbox";
37489
38422
  import { HTTPException as HTTPException36 } from "hono/http-exception";
@@ -37507,6 +38440,38 @@ function connectedMachineComputerAccessError(state, requiresControl) {
37507
38440
  }
37508
38441
 
37509
38442
  // src/routes/computer-sessions.ts
38443
+ var MODEL_COMPUTER_FRAME_MAX_BYTES = 256 * 1024;
38444
+ function validateComputerFrameForApi(frame, expected) {
38445
+ return validateComputerControlFrameEvidence(frame, expected);
38446
+ }
38447
+ async function captureModelComputerFrame(sessionClient, expected) {
38448
+ let captured = validateComputerFrameForApi(
38449
+ await sessionClient.capture(expected.targetId, {
38450
+ format: "jpeg",
38451
+ quality: 55,
38452
+ maxWidth: 1024,
38453
+ maxHeight: 768
38454
+ }),
38455
+ expected
38456
+ );
38457
+ if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
38458
+ captured = validateComputerFrameForApi(
38459
+ await sessionClient.capture(expected.targetId, {
38460
+ format: "jpeg",
38461
+ quality: 30,
38462
+ maxWidth: 640,
38463
+ maxHeight: 480
38464
+ }),
38465
+ expected
38466
+ );
38467
+ }
38468
+ if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
38469
+ throw new BrowserControlProtocolError2(
38470
+ "computer screenshot could not honor the model image byte bound"
38471
+ );
38472
+ }
38473
+ return captured;
38474
+ }
37510
38475
  function registerComputerSessionRoutes(app, deps) {
37511
38476
  const channelServices = {
37512
38477
  db: deps.db,
@@ -37754,6 +38719,46 @@ function registerComputerSessionRoutes(app, deps) {
37754
38719
  return context.json(result);
37755
38720
  }
37756
38721
  );
38722
+ app.get(
38723
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
38724
+ async (context) => {
38725
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
38726
+ context,
38727
+ "sessions:read"
38728
+ );
38729
+ const targetId = requireOpaqueParam2(context, "targetId");
38730
+ const frame = await withActiveComputerController(
38731
+ context,
38732
+ grant,
38733
+ workspaceId,
38734
+ computerSessionId,
38735
+ "session.read",
38736
+ "computer.read",
38737
+ async ({ sessionClient, binding }) => {
38738
+ try {
38739
+ return await captureModelComputerFrame(sessionClient, {
38740
+ computerSessionId,
38741
+ controllerGeneration: binding.controllerGeneration,
38742
+ targetId
38743
+ });
38744
+ } catch (error) {
38745
+ if (error instanceof ComputerFrameEvidenceMismatchError) {
38746
+ observeComputerFrameEvidenceMismatch(deps.observability, error.reason);
38747
+ }
38748
+ throw error;
38749
+ }
38750
+ }
38751
+ );
38752
+ return new Response(frame.data.slice().buffer, {
38753
+ status: 200,
38754
+ headers: {
38755
+ "cache-control": "no-store",
38756
+ "content-type": frame.mediaType,
38757
+ "x-opengeni-computer-frame": frame.metadataHeader
38758
+ }
38759
+ });
38760
+ }
38761
+ );
37757
38762
  app.get(
37758
38763
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
37759
38764
  async (context) => {
@@ -39614,7 +40619,7 @@ import {
39614
40619
  } from "@opengeni/contracts/personal-github";
39615
40620
  import {
39616
40621
  requireAccessGrant as requireAccessGrant17,
39617
- requireAccessGrantAuthorization as requireAccessGrantAuthorization6
40622
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization7
39618
40623
  } from "@opengeni/core";
39619
40624
  import {
39620
40625
  getPersonalGitHubRepositorySelectionState as getPersonalGitHubRepositorySelectionState2,
@@ -39982,7 +40987,7 @@ function registerPersonalGitHubRoutes(app, deps) {
39982
40987
  });
39983
40988
  app.post("/v1/workspaces/:workspaceId/connections/github/oauth/start", async (c) => {
39984
40989
  const workspaceId = c.req.param("workspaceId");
39985
- const access = await requireAccessGrantAuthorization6(c, deps, workspaceId, "connections:write");
40990
+ const access = await requireAccessGrantAuthorization7(c, deps, workspaceId, "connections:write");
39986
40991
  assertPersonalConnectionOwnerPrincipal(access, "My GitHub account");
39987
40992
  const payload = PersonalGitHubOAuthStartRequest.parse(await c.req.json().catch(() => ({})));
39988
40993
  return c.json(
@@ -39996,7 +41001,7 @@ function registerPersonalGitHubRoutes(app, deps) {
39996
41001
  });
39997
41002
  app.post("/v1/workspaces/:workspaceId/connections/:connectionId/github/reconnect", async (c) => {
39998
41003
  const workspaceId = c.req.param("workspaceId");
39999
- const access = await requireAccessGrantAuthorization6(c, deps, workspaceId, "connections:write");
41004
+ const access = await requireAccessGrantAuthorization7(c, deps, workspaceId, "connections:write");
40000
41005
  assertPersonalConnectionOwnerPrincipal(access, "My GitHub account");
40001
41006
  const payload = PersonalGitHubOAuthStartRequest.omit({ connectionId: true }).parse(
40002
41007
  await c.req.json().catch(() => ({}))
@@ -40015,7 +41020,7 @@ function registerPersonalGitHubRoutes(app, deps) {
40015
41020
  async (c) => {
40016
41021
  const workspaceId = c.req.param("workspaceId");
40017
41022
  const connectionId = c.req.param("connectionId");
40018
- const access = await requireAccessGrantAuthorization6(
41023
+ const access = await requireAccessGrantAuthorization7(
40019
41024
  c,
40020
41025
  deps,
40021
41026
  workspaceId,
@@ -40066,7 +41071,7 @@ function registerPersonalGitHubRoutes(app, deps) {
40066
41071
  async (c) => {
40067
41072
  const workspaceId = c.req.param("workspaceId");
40068
41073
  const connectionId = c.req.param("connectionId");
40069
- const access = await requireAccessGrantAuthorization6(
41074
+ const access = await requireAccessGrantAuthorization7(
40070
41075
  c,
40071
41076
  deps,
40072
41077
  workspaceId,
@@ -40113,7 +41118,7 @@ function registerPersonalGitHubRoutes(app, deps) {
40113
41118
  async (c) => {
40114
41119
  const workspaceId = c.req.param("workspaceId");
40115
41120
  const connectionId = c.req.param("connectionId");
40116
- const access = await requireAccessGrantAuthorization6(
41121
+ const access = await requireAccessGrantAuthorization7(
40117
41122
  c,
40118
41123
  deps,
40119
41124
  workspaceId,
@@ -41132,7 +42137,7 @@ function registerApiIntegrationRoutes(app, deps, overrides = {}) {
41132
42137
  });
41133
42138
  app.post("/v1/workspaces/:workspaceId/integrations/install", async (c) => {
41134
42139
  const workspaceId = c.req.param("workspaceId");
41135
- const grant = await requireAccessGrant18(c, deps, workspaceId, "workspace:admin");
42140
+ const grant = await requireAccessGrant18(c, deps, workspaceId, "capabilities:manage");
41136
42141
  const payload = InstallApiIntegrationRequest.parse(await c.req.json());
41137
42142
  const resolved = await resolveForRoute({
41138
42143
  deps,
@@ -41227,7 +42232,7 @@ function registerApiIntegrationRoutes(app, deps, overrides = {}) {
41227
42232
  "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
41228
42233
  async (c) => {
41229
42234
  const workspaceId = c.req.param("workspaceId");
41230
- const grant = await requireAccessGrant18(c, deps, workspaceId, "workspace:admin");
42235
+ const grant = await requireAccessGrant18(c, deps, workspaceId, "capabilities:manage");
41231
42236
  const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
41232
42237
  const instanceKey = decodeURIComponent(c.req.param("instanceKey"));
41233
42238
  const payload = UninstallApiIntegrationRequest.parse(await c.req.json());
@@ -41353,7 +42358,7 @@ import {
41353
42358
  import {
41354
42359
  hasPermission as hasPermission16,
41355
42360
  requireAccessGrant as requireAccessGrant19,
41356
- requireAccessGrantAuthorization as requireAccessGrantAuthorization7
42361
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization8
41357
42362
  } from "@opengeni/core";
41358
42363
  import {
41359
42364
  configureIntegrationFacet as configureIntegrationFacet2,
@@ -41398,11 +42403,11 @@ function registerIntegrationFacetRoutes(app, deps) {
41398
42403
  "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/source",
41399
42404
  async (c) => {
41400
42405
  const workspaceId = c.req.param("workspaceId");
41401
- const authorization = await requireAccessGrantAuthorization7(
42406
+ const authorization = await requireAccessGrantAuthorization8(
41402
42407
  c,
41403
42408
  deps,
41404
42409
  workspaceId,
41405
- "workspace:admin"
42410
+ "capabilities:manage"
41406
42411
  );
41407
42412
  const { grant } = authorization;
41408
42413
  try {
@@ -41417,7 +42422,10 @@ function registerIntegrationFacetRoutes(app, deps) {
41417
42422
  facetKey: decoded(c.req.param("facetKey")),
41418
42423
  payload: await c.req.json(),
41419
42424
  canManageOrganizationDestination: authorization.accountGrant?.permissions.includes("account:admin") === true,
41420
- canManageWorkspaceDestination: hasPermission16(grant.permissions, "workspace:admin"),
42425
+ canManageWorkspaceDestination: hasPermission16(
42426
+ grant.permissions,
42427
+ "capabilities:manage"
42428
+ ),
41421
42429
  canManagePersonalDestination: authorization.contextIntegrity && authorization.authenticatedSubjectId === grant.subjectId
41422
42430
  })
41423
42431
  )
@@ -41453,7 +42461,7 @@ function registerIntegrationFacetRoutes(app, deps) {
41453
42461
  "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
41454
42462
  async (c) => {
41455
42463
  const workspaceId = c.req.param("workspaceId");
41456
- const grant = await requireAccessGrant19(c, deps, workspaceId, "workspace:admin");
42464
+ const grant = await requireAccessGrant19(c, deps, workspaceId, "capabilities:manage");
41457
42465
  const payload = UpsertIntegrationFacetRequest.parse(await c.req.json());
41458
42466
  const capabilityId = decoded(c.req.param("capabilityId"));
41459
42467
  const instanceKey = decoded(c.req.param("instanceKey"));
@@ -41525,7 +42533,7 @@ function registerIntegrationFacetRoutes(app, deps) {
41525
42533
  `/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/${action}`,
41526
42534
  async (c) => {
41527
42535
  const workspaceId = c.req.param("workspaceId");
41528
- const grant = await requireAccessGrant19(c, deps, workspaceId, "workspace:admin");
42536
+ const grant = await requireAccessGrant19(c, deps, workspaceId, "capabilities:manage");
41529
42537
  const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
41530
42538
  try {
41531
42539
  return c.json(
@@ -41553,7 +42561,7 @@ function registerIntegrationFacetRoutes(app, deps) {
41553
42561
  "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
41554
42562
  async (c) => {
41555
42563
  const workspaceId = c.req.param("workspaceId");
41556
- const grant = await requireAccessGrant19(c, deps, workspaceId, "workspace:admin");
42564
+ const grant = await requireAccessGrant19(c, deps, workspaceId, "capabilities:manage");
41557
42565
  const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
41558
42566
  try {
41559
42567
  return c.json(
@@ -41676,6 +42684,8 @@ var WORKSPACE_CONTROL_REPLAY_PAGE_SIZE = 100;
41676
42684
  var SSE_QUEUED_FRAME_MAX_COUNT = 1;
41677
42685
  var SSE_WRITE_STALL_TIMEOUT_MS = 3e4;
41678
42686
  var SSE_HEARTBEAT_INTERVAL_MS = 15e3;
42687
+ var HTTP1_BROWSER_SSE_BATCH_MAX_BYTES = 512 * 1024;
42688
+ var HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE = "application/vnd.opengeni.sse-batch";
41679
42689
  var activeSseStreams = {
41680
42690
  session: 0,
41681
42691
  workspace_control: 0,
@@ -41684,16 +42694,21 @@ var activeSseStreams = {
41684
42694
  function createByteBoundedSseStream(options = {}) {
41685
42695
  const maxQueuedBytes = options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES;
41686
42696
  const stallTimeoutMs = options.stallTimeoutMs ?? SSE_WRITE_STALL_TIMEOUT_MS;
42697
+ const connectionLifetimeMs = options.connectionLifetimeMs;
41687
42698
  if (!Number.isSafeInteger(maxQueuedBytes) || maxQueuedBytes <= 0) {
41688
42699
  throw new RangeError("SSE byte high-water mark must be a positive safe integer");
41689
42700
  }
41690
42701
  if (!Number.isSafeInteger(stallTimeoutMs) || stallTimeoutMs <= 0) {
41691
42702
  throw new RangeError("SSE write stall timeout must be a positive safe integer");
41692
42703
  }
42704
+ if (connectionLifetimeMs !== void 0 && (!Number.isSafeInteger(connectionLifetimeMs) || connectionLifetimeMs <= 0)) {
42705
+ throw new RangeError("SSE connection lifetime must be a positive safe integer");
42706
+ }
41693
42707
  const encoder4 = new TextEncoder();
41694
42708
  let controller;
41695
42709
  let stopped = false;
41696
42710
  let capacityWake = null;
42711
+ let lifetimeTimer = null;
41697
42712
  let queuedFrames = 0;
41698
42713
  let queuedBytes = 0;
41699
42714
  const wakeWriter = () => {
@@ -41704,6 +42719,10 @@ function createByteBoundedSseStream(options = {}) {
41704
42719
  const stop = (settle) => {
41705
42720
  if (stopped) return;
41706
42721
  stopped = true;
42722
+ if (lifetimeTimer !== null) {
42723
+ clearTimeout(lifetimeTimer);
42724
+ lifetimeTimer = null;
42725
+ }
41707
42726
  wakeWriter();
41708
42727
  options.onStop?.();
41709
42728
  try {
@@ -41733,6 +42752,9 @@ function createByteBoundedSseStream(options = {}) {
41733
42752
  size: () => 1
41734
42753
  }
41735
42754
  );
42755
+ if (connectionLifetimeMs !== void 0) {
42756
+ lifetimeTimer = setTimeout(() => stop(() => controller.close()), connectionLifetimeMs);
42757
+ }
41736
42758
  return {
41737
42759
  stream,
41738
42760
  write: async (frame) => {
@@ -41845,6 +42867,17 @@ function createLatestWinsDelivery(send, onError) {
41845
42867
  };
41846
42868
  }
41847
42869
  async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal, options = {}) {
42870
+ if (isHttp1BrowserBatch(options)) {
42871
+ const events = await listSessionEvents(db, workspaceId, sessionId, {
42872
+ after,
42873
+ limit: SESSION_REPLAY_PAGE_SIZE
42874
+ });
42875
+ await options.reauthorize?.();
42876
+ return finiteSseBatchResponse(
42877
+ coalesceSessionEventDeltas(events).map(formatSessionEventSse),
42878
+ options
42879
+ );
42880
+ }
41848
42881
  const durableFanout = requireSessionEventDurableFanoutCapability(bus);
41849
42882
  const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
41850
42883
  let lastSent = after;
@@ -41878,6 +42911,7 @@ async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal,
41878
42911
  release?.();
41879
42912
  };
41880
42913
  const channel = createByteBoundedSseStream({
42914
+ connectionLifetimeMs: options.connectionLifetimeMs,
41881
42915
  maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
41882
42916
  ...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
41883
42917
  onObservation: sseObservationReporter("session", options),
@@ -41887,11 +42921,11 @@ async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal,
41887
42921
  const fail = (error) => {
41888
42922
  channel.fail(retryableSseFailure("session event stream delivery failed", error));
41889
42923
  };
41890
- stopReauthorization = startSseReauthorization(options, channel.stopped, fail);
42924
+ stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
41891
42925
  let writeTail = Promise.resolve();
41892
42926
  const writeFrame = (frame) => {
41893
42927
  const write = writeTail.then(async () => {
41894
- await options.reauthorize?.();
42928
+ await reauthorizeSseOrClose(options, channel);
41895
42929
  if (!await channel.write(frame)) throw new SseStreamStoppedError();
41896
42930
  });
41897
42931
  writeTail = write.catch(() => {
@@ -42010,14 +43044,7 @@ async function sseSessionStream(db, bus, workspaceId, sessionId, after, signal,
42010
43044
  signal.addEventListener("abort", abort, { once: true });
42011
43045
  detachAbortListener = () => signal.removeEventListener("abort", abort);
42012
43046
  }
42013
- return new Response(channel.stream, {
42014
- headers: {
42015
- "Content-Type": "text/event-stream; charset=utf-8",
42016
- "Cache-Control": "no-cache, no-transform",
42017
- Connection: "keep-alive",
42018
- ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
42019
- }
42020
- });
43047
+ return await sseHttpResponse(channel.stream, options);
42021
43048
  }
42022
43049
  async function replaySessionEvents(loadPage, send, after, pageSize = SESSION_REPLAY_PAGE_SIZE) {
42023
43050
  let cursor = after;
@@ -42043,6 +43070,19 @@ async function replaySessionEvents(loadPage, send, after, pageSize = SESSION_REP
42043
43070
  }
42044
43071
  }
42045
43072
  async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal, options = {}) {
43073
+ if (isHttp1BrowserBatch(options)) {
43074
+ const events = await listWorkspaceControlEvents(
43075
+ db,
43076
+ workspaceId,
43077
+ after,
43078
+ WORKSPACE_CONTROL_REPLAY_PAGE_SIZE
43079
+ );
43080
+ await options.reauthorize?.();
43081
+ return finiteSseBatchResponse(
43082
+ events.sort((left, right) => left.sequence - right.sequence).map(formatWorkspaceControlEventSse),
43083
+ options
43084
+ );
43085
+ }
42046
43086
  const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
42047
43087
  let lastSent = after;
42048
43088
  let bootstrapping = true;
@@ -42070,6 +43110,7 @@ async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal, op
42070
43110
  release?.();
42071
43111
  };
42072
43112
  const channel = createByteBoundedSseStream({
43113
+ connectionLifetimeMs: options.connectionLifetimeMs,
42073
43114
  maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
42074
43115
  ...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
42075
43116
  onObservation: sseObservationReporter("workspace_control", options),
@@ -42079,11 +43120,11 @@ async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal, op
42079
43120
  const fail = (error) => {
42080
43121
  channel.fail(retryableSseFailure("workspace control stream delivery failed", error));
42081
43122
  };
42082
- stopReauthorization = startSseReauthorization(options, channel.stopped, fail);
43123
+ stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
42083
43124
  let writeTail = Promise.resolve();
42084
43125
  const writeFrame = (frame) => {
42085
43126
  const write = writeTail.then(async () => {
42086
- await options.reauthorize?.();
43127
+ await reauthorizeSseOrClose(options, channel);
42087
43128
  if (!await channel.write(frame)) throw new SseStreamStoppedError();
42088
43129
  });
42089
43130
  writeTail = write.catch(() => {
@@ -42168,20 +43209,39 @@ async function sseWorkspaceControlStream(db, bus, workspaceId, after, signal, op
42168
43209
  signal.addEventListener("abort", abort, { once: true });
42169
43210
  detachAbortListener = () => signal.removeEventListener("abort", abort);
42170
43211
  }
42171
- return new Response(channel.stream, {
42172
- headers: {
42173
- "Content-Type": "text/event-stream; charset=utf-8",
42174
- "Cache-Control": "no-cache, no-transform",
42175
- Connection: "keep-alive",
42176
- ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
42177
- }
42178
- });
43212
+ return await sseHttpResponse(channel.stream, options);
42179
43213
  }
42180
43214
  async function sseWorkspaceLiveStream(db, bus, accountId, workspaceId, controlAfter, interactionAfter, signal, options = {}) {
43215
+ if (isHttp1BrowserBatch(options)) {
43216
+ const [controlEvents, interactionState] = await Promise.all([
43217
+ listWorkspaceControlEvents(db, workspaceId, controlAfter, WORKSPACE_CONTROL_REPLAY_PAGE_SIZE),
43218
+ getWorkspaceInteractionRevisionState(db, { accountId, workspaceId })
43219
+ ]);
43220
+ await options.reauthorize?.();
43221
+ const frames = [];
43222
+ if (interactionState.revision > interactionAfter) {
43223
+ frames.push(
43224
+ formatWorkspaceInteractionRevisionSse(
43225
+ WorkspaceInteractionRevisionEvent.parse({
43226
+ workspaceId,
43227
+ sequence: interactionState.revision,
43228
+ revision: interactionState.revision,
43229
+ type: "workspace.interaction.changed",
43230
+ occurredAt: (interactionState.updatedAt ?? /* @__PURE__ */ new Date()).toISOString()
43231
+ })
43232
+ )
43233
+ );
43234
+ }
43235
+ frames.push(
43236
+ ...controlEvents.sort((left, right) => left.sequence - right.sequence).map(formatWorkspaceControlEventSse)
43237
+ );
43238
+ return finiteSseBatchResponse(frames, options);
43239
+ }
42181
43240
  const upstream = new AbortController();
42182
43241
  let stopReauthorization = () => {
42183
43242
  };
42184
43243
  const channel = createByteBoundedSseStream({
43244
+ connectionLifetimeMs: options.connectionLifetimeMs,
42185
43245
  maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
42186
43246
  ...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
42187
43247
  onObservation: sseObservationReporter("workspace_interaction", options),
@@ -42197,18 +43257,19 @@ async function sseWorkspaceLiveStream(db, bus, accountId, workspaceId, controlAf
42197
43257
  };
42198
43258
  if (signal.aborted) abort();
42199
43259
  else signal.addEventListener("abort", abort, { once: true });
42200
- stopReauthorization = startSseReauthorization(options, channel.stopped, (error) => {
42201
- channel.fail(retryableSseFailure("workspace live stream authorization failed", error));
42202
- });
43260
+ stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
42203
43261
  const upstreamOptions = {
42204
43262
  ...options,
43263
+ connectionLifetimeMs: void 0,
43264
+ finiteResponseMaxBytes: void 0,
43265
+ finiteResponseMediaType: void 0,
42205
43266
  reauthorize: void 0,
42206
43267
  reauthorizeAfterMs: void 0
42207
43268
  };
42208
43269
  let writeTail = Promise.resolve(true);
42209
43270
  const write = (frame) => {
42210
43271
  const pending = writeTail.then(async () => {
42211
- await options.reauthorize?.();
43272
+ await reauthorizeSseOrClose(options, channel);
42212
43273
  return await channel.write(frame);
42213
43274
  });
42214
43275
  writeTail = pending.catch(() => false);
@@ -42269,16 +43330,28 @@ async function sseWorkspaceLiveStream(db, bus, accountId, workspaceId, controlAf
42269
43330
  if (!upstream.signal.aborted && !channel.stopped()) channel.fail(error);
42270
43331
  }
42271
43332
  })();
42272
- return new Response(channel.stream, {
42273
- headers: {
42274
- "Content-Type": "text/event-stream; charset=utf-8",
42275
- "Cache-Control": "no-cache, no-transform",
42276
- Connection: "keep-alive",
42277
- ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
42278
- }
42279
- });
43333
+ return await sseHttpResponse(channel.stream, options);
42280
43334
  }
42281
43335
  async function sseWorkspaceInteractionRevisionStream(db, accountId, workspaceId, after, signal, options = {}) {
43336
+ if (isHttp1BrowserBatch(options)) {
43337
+ const state = await getWorkspaceInteractionRevisionState(db, {
43338
+ accountId,
43339
+ workspaceId
43340
+ });
43341
+ await options.reauthorize?.();
43342
+ const frames = state.revision > after ? [
43343
+ formatWorkspaceInteractionRevisionSse(
43344
+ WorkspaceInteractionRevisionEvent.parse({
43345
+ workspaceId,
43346
+ sequence: state.revision,
43347
+ revision: state.revision,
43348
+ type: "workspace.interaction.changed",
43349
+ occurredAt: (state.updatedAt ?? /* @__PURE__ */ new Date()).toISOString()
43350
+ })
43351
+ )
43352
+ ] : [];
43353
+ return finiteSseBatchResponse(frames, options);
43354
+ }
42282
43355
  const heartbeatIntervalMs = resolveHeartbeatInterval(options.heartbeatIntervalMs);
42283
43356
  const pollIntervalMs = resolveInteractionPollInterval(options.pollIntervalMs);
42284
43357
  let lastSent = after;
@@ -42291,6 +43364,7 @@ async function sseWorkspaceInteractionRevisionStream(db, accountId, workspaceId,
42291
43364
  let closeMetrics = () => {
42292
43365
  };
42293
43366
  const channel = createByteBoundedSseStream({
43367
+ connectionLifetimeMs: options.connectionLifetimeMs,
42294
43368
  maxQueuedBytes: options.maxQueuedBytes ?? SESSION_EVENT_SSE_FRAME_MAX_BYTES,
42295
43369
  ...options.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: options.stallTimeoutMs },
42296
43370
  onObservation: sseObservationReporter("workspace_interaction", options),
@@ -42302,11 +43376,9 @@ async function sseWorkspaceInteractionRevisionStream(db, accountId, workspaceId,
42302
43376
  }
42303
43377
  });
42304
43378
  closeMetrics = observeSseConnection("workspace_interaction", after, options.observability);
42305
- stopReauthorization = startSseReauthorization(options, channel.stopped, (error) => {
42306
- channel.fail(retryableSseFailure("workspace interaction stream authorization failed", error));
42307
- });
43379
+ stopReauthorization = startSseReauthorization(options, channel.stopped, channel.close);
42308
43380
  const write = async (frame) => {
42309
- await options.reauthorize?.();
43381
+ await reauthorizeSseOrClose(options, channel);
42310
43382
  const accepted = await channel.write(frame);
42311
43383
  if (accepted) lastWriteAt = Date.now();
42312
43384
  return accepted;
@@ -42316,7 +43388,10 @@ async function sseWorkspaceInteractionRevisionStream(db, accountId, workspaceId,
42316
43388
  if (!await write(": connected\n\n")) return;
42317
43389
  for (; ; ) {
42318
43390
  if (stopRequested || signal.aborted || channel.stopped()) return;
42319
- const state = await getWorkspaceInteractionRevisionState(db, { accountId, workspaceId });
43391
+ const state = await getWorkspaceInteractionRevisionState(db, {
43392
+ accountId,
43393
+ workspaceId
43394
+ });
42320
43395
  if (state.revision > lastSent) {
42321
43396
  const event = WorkspaceInteractionRevisionEvent.parse({
42322
43397
  workspaceId,
@@ -42344,14 +43419,7 @@ async function sseWorkspaceInteractionRevisionStream(db, accountId, workspaceId,
42344
43419
  signal.addEventListener("abort", abort, { once: true });
42345
43420
  detachAbortListener = () => signal.removeEventListener("abort", abort);
42346
43421
  }
42347
- return new Response(channel.stream, {
42348
- headers: {
42349
- "Content-Type": "text/event-stream; charset=utf-8",
42350
- "Cache-Control": "no-cache, no-transform",
42351
- Connection: "keep-alive",
42352
- ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
42353
- }
42354
- });
43422
+ return await sseHttpResponse(channel.stream, options);
42355
43423
  }
42356
43424
  function observeSseConnection(stream, after, observability) {
42357
43425
  activeSseStreams[stream] += 1;
@@ -42400,6 +43468,117 @@ async function replayWorkspaceControlEvents(loadPage, send, after, pageSize) {
42400
43468
  }
42401
43469
  var SseStreamStoppedError = class extends Error {
42402
43470
  };
43471
+ async function reauthorizeSseOrClose(options, channel) {
43472
+ try {
43473
+ await options.reauthorize?.();
43474
+ } catch {
43475
+ channel.close();
43476
+ throw new SseStreamStoppedError();
43477
+ }
43478
+ }
43479
+ function browserSseDeliveryOptions(transport) {
43480
+ return transport === "http1-bounded" ? {
43481
+ finiteResponseMaxBytes: HTTP1_BROWSER_SSE_BATCH_MAX_BYTES,
43482
+ finiteResponseMediaType: "http1-browser-batch"
43483
+ } : {};
43484
+ }
43485
+ async function sseHttpResponse(stream, options) {
43486
+ const maxBytes = options.finiteResponseMaxBytes;
43487
+ let body4 = stream;
43488
+ let contentLength = null;
43489
+ if (maxBytes !== void 0) {
43490
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < SESSION_EVENT_SSE_FRAME_MAX_BYTES || maxBytes > HTTP1_BROWSER_SSE_BATCH_MAX_BYTES) {
43491
+ throw new RangeError(
43492
+ `finite SSE batch limit must be between ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} and ${HTTP1_BROWSER_SSE_BATCH_MAX_BYTES} bytes`
43493
+ );
43494
+ }
43495
+ body4 = await collectFiniteSseBatch(stream, maxBytes);
43496
+ contentLength = body4.byteLength;
43497
+ }
43498
+ return new Response(body4, {
43499
+ headers: {
43500
+ // A bounded HTTP/1 poll carries the same SSE-framed bytes the SDK
43501
+ // already parses, but it is an ordinary finite response at the browser
43502
+ // transport boundary. Keeping `text/event-stream` here lets Chromium
43503
+ // retain an orphaned fetch in its shared per-origin SSE pool after the
43504
+ // initiating document is replaced, starving unrelated finite reads in
43505
+ // every tab. HTTP/2 and other unbounded streams keep the standard media
43506
+ // type; only the explicit `http1-bounded` fallback uses this vendor type.
43507
+ "Content-Type": contentLength !== null && options.finiteResponseMediaType === "http1-browser-batch" ? `${HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE}; charset=utf-8` : "text/event-stream; charset=utf-8",
43508
+ "Cache-Control": "no-cache, no-transform",
43509
+ // A known-length response is already terminal and leaves the HTTP/1
43510
+ // socket reusable. Only a genuinely live SSE response needs an explicit
43511
+ // close when its stream ends.
43512
+ ...contentLength === null ? { Connection: "close" } : { "Content-Length": String(contentLength) },
43513
+ ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
43514
+ }
43515
+ });
43516
+ }
43517
+ function isHttp1BrowserBatch(options) {
43518
+ return options.finiteResponseMediaType === "http1-browser-batch" && options.finiteResponseMaxBytes !== void 0;
43519
+ }
43520
+ function finiteSseBatchResponse(frames, options) {
43521
+ const maxBytes = options.finiteResponseMaxBytes;
43522
+ if (maxBytes === void 0 || !Number.isSafeInteger(maxBytes) || maxBytes < SESSION_EVENT_SSE_FRAME_MAX_BYTES || maxBytes > HTTP1_BROWSER_SSE_BATCH_MAX_BYTES) {
43523
+ throw new RangeError(
43524
+ `finite SSE batch limit must be between ${SESSION_EVENT_SSE_FRAME_MAX_BYTES} and ${HTTP1_BROWSER_SSE_BATCH_MAX_BYTES} bytes`
43525
+ );
43526
+ }
43527
+ const encoder4 = new TextEncoder();
43528
+ const chunks = [];
43529
+ let length = 0;
43530
+ for (const frame of frames) {
43531
+ const chunk = encoder4.encode(frame);
43532
+ if (chunk.byteLength > maxBytes) {
43533
+ throw new RangeError(
43534
+ `SSE frame cannot fit in the finite browser batch (${chunk.byteLength} > ${maxBytes} bytes)`
43535
+ );
43536
+ }
43537
+ if (length + chunk.byteLength > maxBytes) break;
43538
+ chunks.push(chunk);
43539
+ length += chunk.byteLength;
43540
+ }
43541
+ const body4 = new Uint8Array(length);
43542
+ let offset = 0;
43543
+ for (const chunk of chunks) {
43544
+ body4.set(chunk, offset);
43545
+ offset += chunk.byteLength;
43546
+ }
43547
+ return new Response(body4, {
43548
+ headers: {
43549
+ "Content-Type": `${HTTP1_BROWSER_SSE_BATCH_CONTENT_TYPE}; charset=utf-8`,
43550
+ "Cache-Control": "no-cache, no-transform",
43551
+ "Content-Length": String(body4.byteLength),
43552
+ ...options.actorEpoch ? { [MANAGED_AUTH_ACTOR_EPOCH_HEADER]: options.actorEpoch } : {}
43553
+ }
43554
+ });
43555
+ }
43556
+ async function collectFiniteSseBatch(stream, maxBytes) {
43557
+ const reader = stream.getReader();
43558
+ const chunks = [];
43559
+ let length = 0;
43560
+ try {
43561
+ for (; ; ) {
43562
+ const next = await reader.read();
43563
+ if (next.done) break;
43564
+ if (length + next.value.byteLength > maxBytes) {
43565
+ await reader.cancel("finite SSE batch reached its byte limit");
43566
+ break;
43567
+ }
43568
+ chunks.push(next.value);
43569
+ length += next.value.byteLength;
43570
+ }
43571
+ } finally {
43572
+ reader.releaseLock();
43573
+ }
43574
+ const bytes = new Uint8Array(length);
43575
+ let offset = 0;
43576
+ for (const chunk of chunks) {
43577
+ bytes.set(chunk, offset);
43578
+ offset += chunk.byteLength;
43579
+ }
43580
+ return bytes.buffer;
43581
+ }
42403
43582
  function sseObservationReporter(stream, options) {
42404
43583
  return (observation) => {
42405
43584
  options.onObservation?.(observation);
@@ -42492,6 +43671,7 @@ function registerInteractionResourceRoutes(app, deps) {
42492
43671
  nonnegativeSafeIntegerQuery(context, "interactionAfter", 0),
42493
43672
  context.req.raw.signal,
42494
43673
  {
43674
+ ...browserSseDeliveryOptions(context.req.query("transport")),
42495
43675
  observability: deps.observability,
42496
43676
  actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? void 0,
42497
43677
  reauthorize: async () => {
@@ -42516,6 +43696,7 @@ function registerInteractionResourceRoutes(app, deps) {
42516
43696
  after,
42517
43697
  context.req.raw.signal,
42518
43698
  {
43699
+ ...browserSseDeliveryOptions(context.req.query("transport")),
42519
43700
  observability: deps.observability,
42520
43701
  actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? void 0,
42521
43702
  reauthorize: async () => {
@@ -43002,7 +44183,7 @@ import {
43002
44183
  getPackInstallation as getPackInstallation2,
43003
44184
  listPrReviewAppRegistrations as listPrReviewAppRegistrations2,
43004
44185
  listPrReviewRepositoryBindings as listPrReviewRepositoryBindings2,
43005
- nestedPostgresSqlState as nestedPostgresSqlState2,
44186
+ nestedPostgresSqlState as nestedPostgresSqlState3,
43006
44187
  recordAuditEvent as recordAuditEvent9,
43007
44188
  updatePrReviewAppRegistration,
43008
44189
  updatePrReviewRepositoryBinding
@@ -43220,7 +44401,7 @@ import {
43220
44401
  getPackInstallation,
43221
44402
  listPrReviewAppRegistrations,
43222
44403
  listPrReviewRepositoryBindings,
43223
- nestedPostgresSqlState,
44404
+ nestedPostgresSqlState as nestedPostgresSqlState2,
43224
44405
  PrReviewDispatchAuthorityError,
43225
44406
  recordAuditEvent as recordAuditEvent8,
43226
44407
  resolveManagedGitHubPrReviewRoute,
@@ -44071,7 +45252,7 @@ function registerPrReviewGitHubRoutes(app, deps) {
44071
45252
  if (error instanceof PrReviewDispatchAuthorityError) {
44072
45253
  throw new HTTPException44(409, { message: error.message });
44073
45254
  }
44074
- if (nestedPostgresSqlState(error) === "23505") {
45255
+ if (nestedPostgresSqlState2(error) === "23505") {
44075
45256
  throw new HTTPException44(409, {
44076
45257
  message: "One of these repositories is already connected to OpenGeni Lens in another workspace"
44077
45258
  });
@@ -44777,7 +45958,7 @@ async function mapPrReviewUniqueConflict(operation, message) {
44777
45958
  try {
44778
45959
  return await operation;
44779
45960
  } catch (error) {
44780
- if (nestedPostgresSqlState2(error) === "23505") {
45961
+ if (nestedPostgresSqlState3(error) === "23505") {
44781
45962
  throw new HTTPException45(409, { message });
44782
45963
  }
44783
45964
  throw error;
@@ -44869,7 +46050,7 @@ function registerPackRoutes(app, deps) {
44869
46050
  });
44870
46051
  app.post("/v1/workspaces/:workspaceId/packs", async (c) => {
44871
46052
  const workspaceId = c.req.param("workspaceId");
44872
- const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
46053
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "capabilities:manage");
44873
46054
  const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
44874
46055
  if (isBuiltInCapabilityPack(manifest.id)) {
44875
46056
  throw new HTTPException46(409, {
@@ -44885,7 +46066,7 @@ function registerPackRoutes(app, deps) {
44885
46066
  });
44886
46067
  app.delete("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
44887
46068
  const workspaceId = c.req.param("workspaceId");
44888
- await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
46069
+ await requireAccessGrant24(c, deps, workspaceId, "capabilities:manage");
44889
46070
  const packId = c.req.param("packId");
44890
46071
  if (isBuiltInCapabilityPack(packId)) {
44891
46072
  throw new HTTPException46(409, {
@@ -44945,7 +46126,7 @@ function registerPackRoutes(app, deps) {
44945
46126
  });
44946
46127
  app.post("/v1/workspaces/:workspaceId/packs/:packId/install", async (c) => {
44947
46128
  const workspaceId = c.req.param("workspaceId");
44948
- const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
46129
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "capabilities:manage");
44949
46130
  const pack = await requirePack(db, workspaceId, c.req.param("packId"));
44950
46131
  const payload = InstallPackRequest.parse(await c.req.json());
44951
46132
  const preview = PackInstallationPreview.parse(
@@ -45154,7 +46335,7 @@ function registerPackRoutes(app, deps) {
45154
46335
  });
45155
46336
  app.delete("/v1/workspaces/:workspaceId/packs/:packId/installation", async (c) => {
45156
46337
  const workspaceId = c.req.param("workspaceId");
45157
- const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
46338
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "capabilities:manage");
45158
46339
  const pack = await requirePack(db, workspaceId, c.req.param("packId"));
45159
46340
  const payload = UninstallPackRequest.parse(await c.req.json());
45160
46341
  const requestDigest = sha2562(
@@ -45240,7 +46421,7 @@ function registerPackRoutes(app, deps) {
45240
46421
  });
45241
46422
  app.post("/v1/workspaces/:workspaceId/packs/:packId/enable", async (c) => {
45242
46423
  const workspaceId = c.req.param("workspaceId");
45243
- const grant = await requireAccessGrant24(c, deps, workspaceId, "workspace:admin");
46424
+ const grant = await requireAccessGrant24(c, deps, workspaceId, "capabilities:manage");
45244
46425
  const pack = await requirePack(db, workspaceId, c.req.param("packId"));
45245
46426
  if (capabilityPackRequiresInstallationPlan(pack)) {
45246
46427
  throw new HTTPException46(409, {
@@ -45640,7 +46821,7 @@ function registerPluginRoutes(app, deps, overrides = {}) {
45640
46821
  });
45641
46822
  app.post("/v1/workspaces/:workspaceId/plugins/install", async (c) => {
45642
46823
  const workspaceId = c.req.param("workspaceId");
45643
- const grant = await requireAccessGrant25(c, deps, workspaceId, "workspace:admin");
46824
+ const grant = await requireAccessGrant25(c, deps, workspaceId, "capabilities:manage");
45644
46825
  const payload = InstallPluginRequest.parse(await c.req.json());
45645
46826
  const resolved = await resolvePluginPackage({
45646
46827
  deps,
@@ -45766,7 +46947,7 @@ function registerPluginRoutes(app, deps, overrides = {}) {
45766
46947
  });
45767
46948
  app.delete("/v1/workspaces/:workspaceId/plugins/:pluginKey", async (c) => {
45768
46949
  const workspaceId = c.req.param("workspaceId");
45769
- const grant = await requireAccessGrant25(c, deps, workspaceId, "workspace:admin");
46950
+ const grant = await requireAccessGrant25(c, deps, workspaceId, "capabilities:manage");
45770
46951
  const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
45771
46952
  const payload = UninstallPluginRequest.parse(await c.req.json());
45772
46953
  try {
@@ -46222,7 +47403,7 @@ function registerSkillRoutes(app, deps, overrides = {}) {
46222
47403
  });
46223
47404
  app.post("/v1/workspaces/:workspaceId/skills/library/:libraryId/install", async (c) => {
46224
47405
  const workspaceId = c.req.param("workspaceId");
46225
- const grant = await requireAccessGrant26(c, deps, workspaceId, "workspace:admin");
47406
+ const grant = await requireAccessGrant26(c, deps, workspaceId, "capabilities:manage");
46226
47407
  const libraryId = decodeURIComponent(c.req.param("libraryId"));
46227
47408
  const payload = InstallLibrarySkillRequest.parse(await c.req.json());
46228
47409
  let loaded;
@@ -46298,7 +47479,7 @@ function registerSkillRoutes(app, deps, overrides = {}) {
46298
47479
  });
46299
47480
  app.post("/v1/workspaces/:workspaceId/skills/install", async (c) => {
46300
47481
  const workspaceId = c.req.param("workspaceId");
46301
- const grant = await requireAccessGrant26(c, deps, workspaceId, "workspace:admin");
47482
+ const grant = await requireAccessGrant26(c, deps, workspaceId, "capabilities:manage");
46302
47483
  const payload = InstallSkillRequest.parse(await c.req.json());
46303
47484
  const resolved = await resolveForRoute2(payload.url, github);
46304
47485
  if (resolved.preview.sourceCommit !== payload.expectedSourceCommit || resolved.preview.contentSha256 !== payload.expectedContentSha256) {
@@ -46358,7 +47539,7 @@ function registerSkillRoutes(app, deps, overrides = {}) {
46358
47539
  });
46359
47540
  app.delete("/v1/workspaces/:workspaceId/skills/:capabilityId", async (c) => {
46360
47541
  const workspaceId = c.req.param("workspaceId");
46361
- const grant = await requireAccessGrant26(c, deps, workspaceId, "workspace:admin");
47542
+ const grant = await requireAccessGrant26(c, deps, workspaceId, "capabilities:manage");
46362
47543
  const capabilityId = decodeURIComponent(c.req.param("capabilityId"));
46363
47544
  const payload = UninstallSkillRequest.parse(await c.req.json());
46364
47545
  try {
@@ -46512,7 +47693,7 @@ import {
46512
47693
  import { HTTPException as HTTPException50 } from "hono/http-exception";
46513
47694
  import {
46514
47695
  requireAccessGrant as requireAccessGrant28,
46515
- requireAccessGrantAuthorization as requireAccessGrantAuthorization8,
47696
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization9,
46516
47697
  requirePermission as requirePermission7
46517
47698
  } from "@opengeni/core";
46518
47699
  import {
@@ -46553,7 +47734,7 @@ async function parseRigRequest(c, schema, label) {
46553
47734
  function registerRigRoutes(app, deps) {
46554
47735
  const { db, workflowClient } = deps;
46555
47736
  async function requireRigMutation(c, workspaceId, permission) {
46556
- const authorization = await requireAccessGrantAuthorization8(c, deps, workspaceId, permission);
47737
+ const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId, permission);
46557
47738
  const rigId = c.req.param("rigId");
46558
47739
  if (!rigId) {
46559
47740
  throw new HTTPException50(400, { message: "rig id is required" });
@@ -46631,7 +47812,7 @@ function registerRigRoutes(app, deps) {
46631
47812
  });
46632
47813
  app.post("/v1/workspaces/:workspaceId/rigs", async (c) => {
46633
47814
  const workspaceId = c.req.param("workspaceId");
46634
- const authorization = await requireAccessGrantAuthorization8(c, deps, workspaceId);
47815
+ const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId);
46635
47816
  const grant = authorization.grant;
46636
47817
  requirePermission7(grant, "rigs:manage");
46637
47818
  const payload = await parseRigRequest(c, CreateRigRequest, "Rig create request");
@@ -46657,7 +47838,7 @@ function registerRigRoutes(app, deps) {
46657
47838
  });
46658
47839
  app.patch("/v1/workspaces/:workspaceId/rigs/:rigId", async (c) => {
46659
47840
  const workspaceId = c.req.param("workspaceId");
46660
- const authorization = await requireAccessGrantAuthorization8(c, deps, workspaceId);
47841
+ const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId);
46661
47842
  const grant = authorization.grant;
46662
47843
  requirePermission7(grant, "rigs:manage");
46663
47844
  const rig = await requireRigForApi2(db, grant, c.req.param("rigId"));
@@ -46672,7 +47853,7 @@ function registerRigRoutes(app, deps) {
46672
47853
  });
46673
47854
  app.delete("/v1/workspaces/:workspaceId/rigs/:rigId", async (c) => {
46674
47855
  const workspaceId = c.req.param("workspaceId");
46675
- const authorization = await requireAccessGrantAuthorization8(c, deps, workspaceId);
47856
+ const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId);
46676
47857
  const grant = authorization.grant;
46677
47858
  requirePermission7(grant, "rigs:manage");
46678
47859
  const rig = await requireRigForApi2(db, grant, c.req.param("rigId"));
@@ -46795,6 +47976,7 @@ import {
46795
47976
  syncCreatedScheduledTask as syncCreatedScheduledTask5,
46796
47977
  syncUpdatedScheduledTask as syncUpdatedScheduledTask4,
46797
47978
  updateScheduledTaskForApi as updateScheduledTaskForApi2,
47979
+ validateScheduledTaskMachineTarget as validateScheduledTaskMachineTarget2,
46798
47980
  validateScheduledTaskTarget as validateScheduledTaskTarget2,
46799
47981
  validatedScheduledTaskUpdate as validatedScheduledTaskUpdate2
46800
47982
  } from "@opengeni/core";
@@ -46918,6 +48100,14 @@ function registerScheduledTaskRoutes(app, deps) {
46918
48100
  agentConfig: task.agentConfig,
46919
48101
  missingTargetStatus: 404
46920
48102
  });
48103
+ await validateScheduledTaskMachineTarget2({
48104
+ settings,
48105
+ db,
48106
+ grant,
48107
+ runMode: task.runMode,
48108
+ agentConfig: task.agentConfig,
48109
+ requireOnline: true
48110
+ });
46921
48111
  await requireLimit8(deps, {
46922
48112
  accountId: grant.accountId,
46923
48113
  workspaceId,
@@ -47095,7 +48285,7 @@ import {
47095
48285
  recordStreamAcknowledgment,
47096
48286
  requestSessionCompaction,
47097
48287
  setSessionCodexPinInTransaction,
47098
- withSessionCodexCapacityMutation,
48288
+ withSessionCodexCapacityMutation as withSessionCodexCapacityMutation2,
47099
48289
  setSessionChannel,
47100
48290
  updateSessionVariableSets,
47101
48291
  ChannelNotFoundError,
@@ -47148,7 +48338,7 @@ import {
47148
48338
  requestSessionBackgroundCommandCancellation
47149
48339
  } from "@opengeni/db/session-background-commands";
47150
48340
  import {
47151
- appendAndPublishEvents as appendAndPublishEvents3,
48341
+ appendAndPublishEvents as appendAndPublishEvents4,
47152
48342
  boundSessionEventHttpPage,
47153
48343
  coalesceSessionEventDeltas as coalesceSessionEventDeltas2,
47154
48344
  publishDurableSessionEvents as publishDurableSessionEvents3
@@ -47901,7 +49091,7 @@ import {
47901
49091
  getManagedAuthRequestActorEpoch as getManagedAuthRequestActorEpoch2,
47902
49092
  hasPermission as hasPermission18,
47903
49093
  requireAccessGrant as requireAccessGrant30,
47904
- requireAccessGrantAuthorization as requireAccessGrantAuthorization9,
49094
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization10,
47905
49095
  requireFreshAccessGrant as requireFreshAccessGrant2,
47906
49096
  requirePermission as requirePermission8,
47907
49097
  requireSessionAuthorization as requireSessionAuthorization7,
@@ -48250,7 +49440,7 @@ function registerSessionRoutes(app, deps) {
48250
49440
  exitCode: process.exitCode,
48251
49441
  reason: process.state === "exited" ? "exit" : "lost"
48252
49442
  };
48253
- await appendAndPublishEvents3(db, bus, ctx.workspaceId, ctx.session.id, [
49443
+ await appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, [
48254
49444
  { type: "terminal.pty.exited", payload: exited }
48255
49445
  ]);
48256
49446
  };
@@ -48331,7 +49521,7 @@ function registerSessionRoutes(app, deps) {
48331
49521
  };
48332
49522
  app.get("/v1/workspaces/:workspaceId/session-tenancy/capabilities", async (c) => {
48333
49523
  const workspaceId = c.req.param("workspaceId");
48334
- const authorization = await requireAccessGrantAuthorization9(
49524
+ const authorization = await requireAccessGrantAuthorization10(
48335
49525
  c,
48336
49526
  deps,
48337
49527
  workspaceId,
@@ -48341,7 +49531,7 @@ function registerSessionRoutes(app, deps) {
48341
49531
  });
48342
49532
  app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
48343
49533
  const workspaceId = c.req.param("workspaceId");
48344
- const authorization = await requireAccessGrantAuthorization9(
49534
+ const authorization = await requireAccessGrantAuthorization10(
48345
49535
  c,
48346
49536
  deps,
48347
49537
  workspaceId,
@@ -48375,7 +49565,7 @@ function registerSessionRoutes(app, deps) {
48375
49565
  });
48376
49566
  app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
48377
49567
  const workspaceId = c.req.param("workspaceId");
48378
- const authorization = await requireAccessGrantAuthorization9(
49568
+ const authorization = await requireAccessGrantAuthorization10(
48379
49569
  c,
48380
49570
  deps,
48381
49571
  workspaceId,
@@ -48429,7 +49619,7 @@ function registerSessionRoutes(app, deps) {
48429
49619
  });
48430
49620
  app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
48431
49621
  const workspaceId = c.req.param("workspaceId");
48432
- const authorization = await requireAccessGrantAuthorization9(
49622
+ const authorization = await requireAccessGrantAuthorization10(
48433
49623
  c,
48434
49624
  deps,
48435
49625
  workspaceId,
@@ -48685,7 +49875,7 @@ function registerSessionRoutes(app, deps) {
48685
49875
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/visibility", async (c) => {
48686
49876
  const workspaceId = c.req.param("workspaceId");
48687
49877
  const sessionId = c.req.param("sessionId");
48688
- const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId);
49878
+ const authorization = await requireAccessGrantAuthorization10(c, deps, workspaceId);
48689
49879
  const parsed = UpdateSessionVisibilityRequest.safeParse(await c.req.json().catch(() => null));
48690
49880
  if (!parsed.success) {
48691
49881
  throw new ApiHttpError(422, {
@@ -48713,7 +49903,7 @@ function registerSessionRoutes(app, deps) {
48713
49903
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/forks", async (c) => {
48714
49904
  const workspaceId = c.req.param("workspaceId");
48715
49905
  const sessionId = c.req.param("sessionId");
48716
- const authorization = await requireAccessGrantAuthorization9(c, deps, workspaceId);
49906
+ const authorization = await requireAccessGrantAuthorization10(c, deps, workspaceId);
48717
49907
  const parsed = ForkSessionRequest.safeParse(await c.req.json().catch(() => null));
48718
49908
  if (!parsed.success) {
48719
49909
  throw new ApiHttpError(422, {
@@ -49356,7 +50546,7 @@ function registerSessionRoutes(app, deps) {
49356
50546
  );
49357
50547
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/pin", async (c) => {
49358
50548
  const workspaceId = c.req.param("workspaceId");
49359
- const authorization = await requireAccessGrantAuthorization9(
50549
+ const authorization = await requireAccessGrantAuthorization10(
49360
50550
  c,
49361
50551
  deps,
49362
50552
  workspaceId,
@@ -49409,7 +50599,7 @@ function registerSessionRoutes(app, deps) {
49409
50599
  });
49410
50600
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
49411
50601
  const workspaceId = c.req.param("workspaceId");
49412
- const authorization = await requireAccessGrantAuthorization9(
50602
+ const authorization = await requireAccessGrantAuthorization10(
49413
50603
  c,
49414
50604
  deps,
49415
50605
  workspaceId,
@@ -49459,7 +50649,7 @@ function registerSessionRoutes(app, deps) {
49459
50649
  });
49460
50650
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
49461
50651
  const workspaceId = c.req.param("workspaceId");
49462
- const authorization = await requireAccessGrantAuthorization9(
50652
+ const authorization = await requireAccessGrantAuthorization10(
49463
50653
  c,
49464
50654
  deps,
49465
50655
  workspaceId,
@@ -49541,7 +50731,7 @@ function registerSessionRoutes(app, deps) {
49541
50731
  }
49542
50732
  }
49543
50733
  const pinned = target === "auto" ? null : target;
49544
- const mutation = await withSessionCodexCapacityMutation(
50734
+ const mutation = await withSessionCodexCapacityMutation2(
49545
50735
  db,
49546
50736
  { workspaceId, reason: "codex_manual_session_pin_changed" },
49547
50737
  async (tx) => {
@@ -50121,7 +51311,7 @@ function registerSessionRoutes(app, deps) {
50121
51311
  }
50122
51312
  throw error;
50123
51313
  });
50124
- await appendAndPublishEvents3(db, bus, workspaceId, sessionId, [
51314
+ await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [
50125
51315
  {
50126
51316
  type: "session.context.cleared",
50127
51317
  payload: {
@@ -50319,6 +51509,7 @@ function registerSessionRoutes(app, deps) {
50319
51509
  Number.isFinite(after) ? after : 0,
50320
51510
  c.req.raw.signal,
50321
51511
  {
51512
+ ...browserSseDeliveryOptions(c.req.query("transport")),
50322
51513
  observability: deps.observability,
50323
51514
  actorEpoch: getManagedAuthRequestActorEpoch2(c.req.raw) ?? void 0,
50324
51515
  reauthorizeAfterMs: authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
@@ -50533,7 +51724,7 @@ function registerSessionRoutes(app, deps) {
50533
51724
  });
50534
51725
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/steer", async (c) => {
50535
51726
  const workspaceId = c.req.param("workspaceId");
50536
- const authorization = await requireAccessGrantAuthorization9(
51727
+ const authorization = await requireAccessGrantAuthorization10(
50537
51728
  c,
50538
51729
  deps,
50539
51730
  workspaceId,
@@ -50565,7 +51756,7 @@ function registerSessionRoutes(app, deps) {
50565
51756
  });
50566
51757
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/composer-draft/submit", async (c) => {
50567
51758
  const workspaceId = c.req.param("workspaceId");
50568
- const authorization = await requireAccessGrantAuthorization9(
51759
+ const authorization = await requireAccessGrantAuthorization10(
50569
51760
  c,
50570
51761
  deps,
50571
51762
  workspaceId,
@@ -50587,7 +51778,7 @@ function registerSessionRoutes(app, deps) {
50587
51778
  });
50588
51779
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
50589
51780
  const workspaceId = c.req.param("workspaceId");
50590
- const authorization = await requireAccessGrantAuthorization9(
51781
+ const authorization = await requireAccessGrantAuthorization10(
50591
51782
  c,
50592
51783
  deps,
50593
51784
  workspaceId,
@@ -50780,12 +51971,8 @@ function registerSessionRoutes(app, deps) {
50780
51971
  // Human take-control: when the desktop is available + this policy is on
50781
51972
  // (default), the cell is mode "interactive" — the noVNC viewer drives :0
50782
51973
  // (x11vnc runs without -viewonly). Off → mode "read-only" (client disables
50783
- // take-control). Independent of the agent's computerUseReadOnly.
51974
+ // take-control). Agent interaction uses managed ComputerSession tools.
50784
51975
  desktopInteractive: settings.sandboxDesktopInteractive,
50785
- // P4.3 computer-use: the agent drives the active display; availability
50786
- // tracks the desktop tier + a desktop-capable backend.
50787
- computerUseEnabled: settings.computerUseEnabled,
50788
- computerUseReadOnly: settings.computerUseReadOnly,
50789
51976
  // Graceful degrade when scoped stream credentials cannot be minted.
50790
51977
  streamTokenSecretAvailable: !streamTokenDegraded(settings),
50791
51978
  desktopAcknowledged: acknowledged,
@@ -51465,7 +52652,7 @@ function registerSessionRoutes(app, deps) {
51465
52652
  };
51466
52653
  events.push({ type: "terminal.pty.output.delta", payload: delta });
51467
52654
  }
51468
- await appendAndPublishEvents3(db, bus, ctx.workspaceId, ctx.session.id, events);
52655
+ await appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, events);
51469
52656
  return opened.response;
51470
52657
  });
51471
52658
  return c.json(out, 201);
@@ -51526,7 +52713,7 @@ function registerSessionRoutes(app, deps) {
51526
52713
  chunk: output,
51527
52714
  seq: seq++
51528
52715
  };
51529
- await appendAndPublishEvents3(db, bus, ctx.workspaceId, ctx.session.id, [
52716
+ await appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, [
51530
52717
  { type: "terminal.pty.output.delta", payload: delta }
51531
52718
  ]);
51532
52719
  }
@@ -52302,7 +53489,7 @@ import {
52302
53489
  } from "@opengeni/db";
52303
53490
  import { HTTPException as HTTPException54 } from "hono/http-exception";
52304
53491
  import { z as z14 } from "zod";
52305
- import { requireAccessGrant as requireAccessGrant31, requireAccessGrantAuthorization as requireAccessGrantAuthorization10 } from "@opengeni/core";
53492
+ import { requireAccessGrant as requireAccessGrant31, requireAccessGrantAuthorization as requireAccessGrantAuthorization11 } from "@opengeni/core";
52306
53493
  function registerSocialRoutes(app, deps) {
52307
53494
  const { db, settings, observability } = deps;
52308
53495
  app.get("/v1/workspaces/:workspaceId/social/connections", async (c) => {
@@ -52372,7 +53559,7 @@ function registerSocialRoutes(app, deps) {
52372
53559
  });
52373
53560
  }
52374
53561
  const payload = parsed.data;
52375
- const access = await requireAccessGrantAuthorization10(
53562
+ const access = await requireAccessGrantAuthorization11(
52376
53563
  c,
52377
53564
  deps,
52378
53565
  workspaceId,
@@ -52490,6 +53677,9 @@ function socialHttpException(error) {
52490
53677
  import {
52491
53678
  AddWorkspaceMemberRequest,
52492
53679
  CreateWorkspaceRequest,
53680
+ EnsureWorkspaceRequest,
53681
+ EnsureWorkspaceResponse,
53682
+ ListWorkspaceMemberCandidatesResponse,
52493
53683
  ListWorkspaceMembersResponse,
52494
53684
  SetWorkspaceDefaultRigRequest,
52495
53685
  UpdateWorkspaceMemberRequest,
@@ -52508,45 +53698,134 @@ import {
52508
53698
  allWorkspacePermissions,
52509
53699
  createWorkspace,
52510
53700
  deleteWorkspaceIfQuiescent,
52511
- getManagedUserByEmail,
53701
+ ensureWorkspaceByExternalIdentity,
53702
+ findWorkspaceByExternalIdentity,
53703
+ getManagedUserProfilesByIds,
52512
53704
  getWorkspaceModelPolicy,
52513
53705
  grantWorkspaceAccess,
52514
53706
  listWorkspaceMembers,
53707
+ listWorkspaceMemberManagementCandidates,
52515
53708
  normalizeWorkspaceMembershipPermissions,
52516
53709
  listWorkspaceControlEvents as listWorkspaceControlEvents2,
53710
+ listSharedWorkspacesForAccount,
52517
53711
  listWorkspacesForSubject,
53712
+ nestedPostgresSqlState as nestedPostgresSqlState4,
52518
53713
  removeWorkspaceMember,
52519
53714
  requireWorkspace,
52520
53715
  getRig,
52521
53716
  setWorkspaceDefaultRig,
52522
53717
  updateWorkspace,
52523
53718
  updateWorkspaceSettings,
53719
+ upsertWorkspaceMemberAsWorkspaceManager,
52524
53720
  upsertWorkspaceModelPolicy,
52525
53721
  workspaceCodexSubscriptionActive,
52526
53722
  workspaceControlRequestLockTimeoutMs as workspaceControlRequestLockTimeoutMs2,
52527
53723
  workspaceXaiSubscriptionActive,
52528
- workspaceVercelAiGatewayConnectionActive
53724
+ workspaceVercelAiGatewayConnectionActive,
53725
+ WorkspaceExternalIdentityConflictError,
53726
+ WorkspaceLimitExceededError
52529
53727
  } from "@opengeni/db";
52530
53728
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
52531
53729
  import { HTTPException as HTTPException55 } from "hono/http-exception";
52532
53730
  import {
52533
53731
  getManagedAuthRequestActorEpoch as getManagedAuthRequestActorEpoch3,
53732
+ accountScopedApiKeyWorkspaceAuthority as accountScopedApiKeyWorkspaceAuthority2,
52534
53733
  hasPermission as hasPermission19,
52535
- requireAccessContext as requireAccessContext2,
53734
+ requireAccessContext as requireAccessContext3,
52536
53735
  requireAccessGrant as requireAccessGrant32,
52537
53736
  requireFreshAccessGrant as requireFreshAccessGrant3
52538
53737
  } from "@opengeni/core";
52539
53738
  import { requireLimit as requireLimit9 } from "@opengeni/core";
52540
53739
  import {
52541
53740
  assertWorkspaceMemberRemovable,
52542
- controlHumanWorkspace,
52543
- resolveMemberSubjectId
53741
+ assertWorkspaceMemberUpdateAllowed,
53742
+ controlHumanWorkspace
52544
53743
  } from "@opengeni/core";
53744
+
53745
+ // src/workspace-delete-observability.ts
53746
+ var DURATION_BUCKETS2 = [
53747
+ 5e-3,
53748
+ 0.01,
53749
+ 0.025,
53750
+ 0.05,
53751
+ 0.1,
53752
+ 0.25,
53753
+ 0.5,
53754
+ 1,
53755
+ 2.5,
53756
+ 5,
53757
+ 10,
53758
+ 30,
53759
+ 60,
53760
+ 120,
53761
+ 300,
53762
+ 600,
53763
+ 1200,
53764
+ 2400,
53765
+ 3600
53766
+ ];
53767
+ var INVENTORY_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1e3, 5e3, 1e4];
53768
+ function recordWorkspaceDeleteObservation(observability, identity, observation) {
53769
+ const labels = { phase: observation.phase, outcome: observation.outcome };
53770
+ observability.observeHistogram({
53771
+ name: "opengeni_workspace_delete_phase_seconds",
53772
+ help: "Workspace deletion transaction and bounded internal phase duration in seconds.",
53773
+ labels,
53774
+ buckets: DURATION_BUCKETS2,
53775
+ value: Math.max(0, observation.durationSeconds)
53776
+ });
53777
+ if (observation.phase === "transaction") {
53778
+ observability.incrementCounter({
53779
+ name: "opengeni_workspace_delete_attempts_total",
53780
+ help: "Workspace deletion attempts by terminal database outcome.",
53781
+ labels: { outcome: observation.outcome }
53782
+ });
53783
+ }
53784
+ for (const [kind, count] of Object.entries(observation.inventory ?? {})) {
53785
+ observability.observeHistogram({
53786
+ name: "opengeni_workspace_delete_inventory_rows",
53787
+ help: "Rows or live owners observed by workspace deletion preflight inventory class.",
53788
+ labels: { kind },
53789
+ buckets: INVENTORY_BUCKETS,
53790
+ value: Math.max(0, Math.floor(count ?? 0))
53791
+ });
53792
+ }
53793
+ observability.info("Workspace deletion phase", {
53794
+ accountId: identity.accountId,
53795
+ workspaceId: identity.workspaceId,
53796
+ phase: observation.phase,
53797
+ outcome: observation.outcome,
53798
+ durationSeconds: observation.durationSeconds,
53799
+ inventoryJson: JSON.stringify(observation.inventory ?? {})
53800
+ });
53801
+ }
53802
+ function workspaceDeleteObserver(observability, identity) {
53803
+ if (!observability) return void 0;
53804
+ return {
53805
+ onPhase: (observation) => {
53806
+ try {
53807
+ recordWorkspaceDeleteObservation(observability, identity, observation);
53808
+ } catch {
53809
+ try {
53810
+ observability.incrementCounter({
53811
+ name: "opengeni_observability_observer_errors_total",
53812
+ help: "Observability observer failures isolated from product execution.",
53813
+ labels: { observer: "workspace_delete" }
53814
+ });
53815
+ } catch {
53816
+ }
53817
+ }
53818
+ }
53819
+ };
53820
+ }
53821
+
53822
+ // src/routes/workspaces.ts
52545
53823
  import {
52546
53824
  AI_GATEWAY_REALTIME_MODELS,
52547
53825
  CODEX_REALTIME_MODEL_ID,
52548
53826
  SUPERGROK_REALTIME_MODEL_ID as SUPERGROK_REALTIME_MODEL_ID2,
52549
- canonicalizeConfiguredModelId
53827
+ canonicalizeConfiguredModelId,
53828
+ configuredStaticUsageLimits as configuredStaticUsageLimits2
52550
53829
  } from "@opengeni/config";
52551
53830
  function canonicalWorkspacePolicyModelIds(settings, modelIds) {
52552
53831
  if (modelIds === null || modelIds === void 0) {
@@ -52567,10 +53846,18 @@ function workspaceUpdateRequestsAccountTransfer(value) {
52567
53846
  }
52568
53847
  function registerWorkspaceRoutes(app, deps) {
52569
53848
  app.get("/v1/access/me", async (c) => {
52570
- return c.json(await requireAccessContext2(c, deps));
53849
+ return c.json(await requireAccessContext3(c, deps));
52571
53850
  });
52572
53851
  app.get("/v1/workspaces", async (c) => {
52573
- const context = await requireAccessContext2(c, deps);
53852
+ const context = await requireAccessContext3(c, deps);
53853
+ const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority2(context);
53854
+ if (accountScopedAuthority && hasPermission19(accountScopedAuthority.permissions, "workspace:read")) {
53855
+ return c.json(
53856
+ (await listSharedWorkspacesForAccount(deps.db, accountScopedAuthority.accountId)).map(
53857
+ (workspace) => Workspace.parse(workspace)
53858
+ )
53859
+ );
53860
+ }
52574
53861
  const readableWorkspaceIds = [
52575
53862
  ...new Set(
52576
53863
  context.workspaceGrants.filter((grant) => hasPermission19(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId)
@@ -52588,32 +53875,96 @@ function registerWorkspaceRoutes(app, deps) {
52588
53875
  )
52589
53876
  );
52590
53877
  });
53878
+ app.put("/v1/workspaces/external", async (c) => {
53879
+ const context = await requireAccessContext3(c, deps);
53880
+ const payload = EnsureWorkspaceRequest.parse(await c.req.json());
53881
+ requireAccountPermission2(context, payload.accountId, "workspace:create");
53882
+ try {
53883
+ const existing = await findWorkspaceByExternalIdentity(deps.db, {
53884
+ externalSource: payload.externalSource,
53885
+ externalId: payload.externalId
53886
+ });
53887
+ if (existing) {
53888
+ if (existing.accountId !== payload.accountId || existing.kind !== "shared") {
53889
+ throw new WorkspaceExternalIdentityConflictError();
53890
+ }
53891
+ return c.json(
53892
+ EnsureWorkspaceResponse.parse({
53893
+ workspace: existing,
53894
+ created: false
53895
+ })
53896
+ );
53897
+ }
53898
+ await requireLimit9(deps, {
53899
+ accountId: payload.accountId,
53900
+ action: "workspace:create",
53901
+ quantity: 1
53902
+ });
53903
+ const result = await ensureWorkspaceByExternalIdentity(deps.db, {
53904
+ accountId: payload.accountId,
53905
+ externalSource: payload.externalSource,
53906
+ externalId: payload.externalId,
53907
+ name: payload.name,
53908
+ slug: payload.slug ?? null,
53909
+ ...payload.agentInstructions !== void 0 ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) } : {},
53910
+ maxWorkspacesPerAccount: workspaceLimit(deps)
53911
+ });
53912
+ const response = EnsureWorkspaceResponse.parse(result);
53913
+ return result.created ? c.json(response, 201) : c.json(response);
53914
+ } catch (error) {
53915
+ if (error instanceof WorkspaceExternalIdentityConflictError) {
53916
+ throw new HTTPException55(409, {
53917
+ message: "external workspace identity is already in use"
53918
+ });
53919
+ }
53920
+ if (error instanceof WorkspaceLimitExceededError) {
53921
+ throw new HTTPException55(429, { message: error.message });
53922
+ }
53923
+ throw error;
53924
+ }
53925
+ });
52591
53926
  app.post("/v1/workspaces", async (c) => {
52592
- const context = await requireAccessContext2(c, deps);
53927
+ const context = await requireAccessContext3(c, deps);
52593
53928
  const payload = CreateWorkspaceRequest.parse(await c.req.json());
52594
53929
  const accountId = payload.accountId ?? context.defaultAccountId;
52595
53930
  if (!accountId) {
52596
- throw new HTTPException55(409, { message: "account selection is required" });
53931
+ throw new HTTPException55(409, {
53932
+ message: "account selection is required"
53933
+ });
52597
53934
  }
52598
- requireAccountPermission(context, accountId, "workspace:create");
52599
- await requireLimit9(deps, { accountId, action: "workspace:create", quantity: 1 });
52600
- const workspace = await createWorkspace(deps.db, {
52601
- accountId,
52602
- name: payload.name.trim(),
52603
- slug: payload.slug?.trim() || null,
52604
- externalSource: payload.externalSource ?? null,
52605
- externalId: payload.externalId ?? null,
52606
- ...payload.agentInstructions !== void 0 ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) } : {}
52607
- });
52608
- await grantWorkspaceAccess(deps.db, {
53935
+ requireAccountPermission2(context, accountId, "workspace:create");
53936
+ await requireLimit9(deps, {
52609
53937
  accountId,
52610
- workspaceId: workspace.id,
52611
- subjectId: context.subjectId,
52612
- role: "owner",
52613
- permissions: allWorkspacePermissions,
52614
- ...context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}
53938
+ action: "workspace:create",
53939
+ quantity: 1
52615
53940
  });
52616
- return c.json(Workspace.parse(workspace), 201);
53941
+ try {
53942
+ const workspace = await createWorkspace(deps.db, {
53943
+ accountId,
53944
+ name: payload.name.trim(),
53945
+ slug: payload.slug?.trim() || null,
53946
+ externalSource: payload.externalSource ?? null,
53947
+ externalId: payload.externalId ?? null,
53948
+ ...payload.agentInstructions !== void 0 ? {
53949
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions)
53950
+ } : {},
53951
+ maxWorkspacesPerAccount: workspaceLimit(deps)
53952
+ });
53953
+ await grantWorkspaceAccess(deps.db, {
53954
+ accountId,
53955
+ workspaceId: workspace.id,
53956
+ subjectId: context.subjectId,
53957
+ role: "owner",
53958
+ permissions: allWorkspacePermissions,
53959
+ ...context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}
53960
+ });
53961
+ return c.json(Workspace.parse(workspace), 201);
53962
+ } catch (error) {
53963
+ if (error instanceof WorkspaceLimitExceededError) {
53964
+ throw new HTTPException55(429, { message: error.message });
53965
+ }
53966
+ throw error;
53967
+ }
52617
53968
  });
52618
53969
  app.get("/v1/workspaces/:workspaceId", async (c) => {
52619
53970
  const workspaceId = c.req.param("workspaceId");
@@ -52637,7 +53988,9 @@ function registerWorkspaceRoutes(app, deps) {
52637
53988
  const workspace = await updateWorkspace(deps.db, workspaceId, {
52638
53989
  ...payload.name !== void 0 ? { name: payload.name.trim() } : {},
52639
53990
  ...payload.slug !== void 0 ? { slug: payload.slug?.trim() || null } : {},
52640
- ...payload.agentInstructions !== void 0 ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) } : {}
53991
+ ...payload.agentInstructions !== void 0 ? {
53992
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions)
53993
+ } : {}
52641
53994
  });
52642
53995
  return c.json(Workspace.parse(workspace));
52643
53996
  });
@@ -52646,7 +53999,9 @@ function registerWorkspaceRoutes(app, deps) {
52646
53999
  await requireAccessGrant32(c, deps, workspaceId, "workspace:admin");
52647
54000
  const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json());
52648
54001
  if (!parsed.success) {
52649
- throw new HTTPException55(400, { message: "invalid workspace settings patch" });
54002
+ throw new HTTPException55(400, {
54003
+ message: "invalid workspace settings patch"
54004
+ });
52650
54005
  }
52651
54006
  const workspace = await updateWorkspaceSettings(deps.db, workspaceId, parsed.data, {
52652
54007
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs2()
@@ -52757,11 +54112,15 @@ function registerWorkspaceRoutes(app, deps) {
52757
54112
  const workspaceId = c.req.param("workspaceId");
52758
54113
  const grant = await requireAccessGrant32(c, deps, workspaceId, "workspace:admin");
52759
54114
  if (workspaceControlUtf8Bytes2(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES2) {
52760
- throw new HTTPException55(400, { message: "workspace-control actor is too large" });
54115
+ throw new HTTPException55(400, {
54116
+ message: "workspace-control actor is too large"
54117
+ });
52761
54118
  }
52762
54119
  const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
52763
54120
  if (!parsed.success) {
52764
- throw new HTTPException55(400, { message: "invalid workspace inference-control request" });
54121
+ throw new HTTPException55(400, {
54122
+ message: "invalid workspace inference-control request"
54123
+ });
52765
54124
  }
52766
54125
  return c.json(
52767
54126
  await controlHumanWorkspace(
@@ -52803,6 +54162,7 @@ function registerWorkspaceRoutes(app, deps) {
52803
54162
  after,
52804
54163
  c.req.raw.signal,
52805
54164
  {
54165
+ ...browserSseDeliveryOptions(c.req.query("transport")),
52806
54166
  observability: deps.observability,
52807
54167
  actorEpoch: getManagedAuthRequestActorEpoch3(c.req.raw) ?? void 0,
52808
54168
  reauthorize: async () => {
@@ -52818,7 +54178,9 @@ function registerWorkspaceRoutes(app, deps) {
52818
54178
  if (payload.rigId) {
52819
54179
  const rig = await getRig(deps.db, workspaceId, payload.rigId);
52820
54180
  if (!rig) {
52821
- throw new HTTPException55(422, { message: `unknown rigId: ${payload.rigId}` });
54181
+ throw new HTTPException55(422, {
54182
+ message: `unknown rigId: ${payload.rigId}`
54183
+ });
52822
54184
  }
52823
54185
  }
52824
54186
  const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId);
@@ -52827,15 +54189,22 @@ function registerWorkspaceRoutes(app, deps) {
52827
54189
  app.delete("/v1/workspaces/:workspaceId", async (c) => {
52828
54190
  const workspaceId = c.req.param("workspaceId");
52829
54191
  const grant = await requireAccessGrant32(c, deps, workspaceId, "workspace:admin");
52830
- const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
54192
+ const deleteObserver = workspaceDeleteObserver(deps.observability, {
52831
54193
  accountId: grant.accountId,
52832
54194
  workspaceId
52833
54195
  });
54196
+ const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
54197
+ accountId: grant.accountId,
54198
+ workspaceId,
54199
+ ...deleteObserver ? { observer: deleteObserver } : {}
54200
+ });
52834
54201
  if (deleted.status === "not_found") {
52835
54202
  throw new HTTPException55(404, { message: "workspace not found" });
52836
54203
  }
52837
54204
  if (deleted.status === "only_workspace") {
52838
- throw new HTTPException55(409, { message: "cannot delete the account's only workspace" });
54205
+ throw new HTTPException55(409, {
54206
+ message: "cannot delete the account's only workspace"
54207
+ });
52839
54208
  }
52840
54209
  if (deleted.status === "active_sessions") {
52841
54210
  throw new HTTPException55(409, {
@@ -52864,7 +54233,9 @@ function registerWorkspaceRoutes(app, deps) {
52864
54233
  {
52865
54234
  db: deps.db,
52866
54235
  deleteSchedule: async (temporalScheduleId) => {
52867
- await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
54236
+ await deps.workflowClient.deleteScheduledTaskSchedule({
54237
+ temporalScheduleId
54238
+ });
52868
54239
  },
52869
54240
  ...deps.observability ? { observability: deps.observability } : {}
52870
54241
  },
@@ -52875,25 +54246,71 @@ function registerWorkspaceRoutes(app, deps) {
52875
54246
  app.get("/v1/workspaces/:workspaceId/members", async (c) => {
52876
54247
  const workspaceId = c.req.param("workspaceId");
52877
54248
  await requireAccessGrant32(c, deps, workspaceId, "workspace:read");
52878
- const members = await listWorkspaceMembers(deps.db, workspaceId);
54249
+ const members = await listWorkspacePeople(deps, workspaceId);
52879
54250
  return c.json(workspaceMembersResponse(members));
52880
54251
  });
54252
+ app.get("/v1/workspaces/:workspaceId/member-candidates", async (c) => {
54253
+ const workspaceId = c.req.param("workspaceId");
54254
+ const grant = await requireAccessGrant32(c, deps, workspaceId, "members:manage");
54255
+ try {
54256
+ return c.json(
54257
+ ListWorkspaceMemberCandidatesResponse.parse({
54258
+ members: await listWorkspaceMemberManagementCandidates(deps.db, {
54259
+ accountId: grant.accountId,
54260
+ workspaceId,
54261
+ actorSubjectId: grant.subjectId
54262
+ })
54263
+ })
54264
+ );
54265
+ } catch (error) {
54266
+ rethrowWorkspaceMemberCandidateError(error);
54267
+ }
54268
+ });
52881
54269
  app.post("/v1/workspaces/:workspaceId/members", async (c) => {
52882
54270
  const workspaceId = c.req.param("workspaceId");
52883
54271
  const grant = await requireAccessGrant32(c, deps, workspaceId, "members:manage");
52884
54272
  const payload = AddWorkspaceMemberRequest.parse(await c.req.json());
52885
- const email = payload.email.trim();
52886
- const subjectId = resolveMemberSubjectId(await getManagedUserByEmail(deps.db, email));
52887
- await grantWorkspaceAccess(deps.db, {
52888
- accountId: grant.accountId,
52889
- workspaceId,
52890
- subjectId,
52891
- subjectLabel: email,
52892
- ...payload.role !== void 0 ? { role: payload.role } : {},
52893
- permissions: payload.permissions
52894
- });
52895
- const members = await listWorkspaceMembers(deps.db, workspaceId);
52896
- const member = members.find((candidate) => candidate.subjectId === subjectId);
54273
+ let candidates;
54274
+ try {
54275
+ candidates = await listWorkspaceMemberManagementCandidates(deps.db, {
54276
+ accountId: grant.accountId,
54277
+ workspaceId,
54278
+ actorSubjectId: grant.subjectId
54279
+ });
54280
+ } catch (error) {
54281
+ rethrowWorkspaceMemberCandidateError(error);
54282
+ }
54283
+ const candidate = candidates.find(
54284
+ (entry2) => entry2.organizationMembershipId === payload.organizationMembershipId
54285
+ );
54286
+ if (!candidate) {
54287
+ throw new HTTPException55(404, {
54288
+ message: "this organization member is not available to add"
54289
+ });
54290
+ }
54291
+ const subjectId = candidate.subjectId;
54292
+ const existing = await listWorkspaceMembers(deps.db, workspaceId);
54293
+ if (existing.some((member2) => member2.subjectId === subjectId)) {
54294
+ throw new HTTPException55(409, {
54295
+ message: "this person already has access to the workspace"
54296
+ });
54297
+ }
54298
+ try {
54299
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
54300
+ accountId: grant.accountId,
54301
+ workspaceId,
54302
+ actorSubjectId: grant.subjectId,
54303
+ targetSubjectId: subjectId,
54304
+ mode: "add",
54305
+ subjectLabel: candidate.name?.trim() || candidate.email || subjectId,
54306
+ role: payload.role ?? "member",
54307
+ permissions: payload.permissions
54308
+ });
54309
+ } catch (error) {
54310
+ rethrowWorkspaceMemberCandidateError(error);
54311
+ }
54312
+ const members = await listWorkspacePeople(deps, workspaceId);
54313
+ const member = members.find((addedMember) => addedMember.subjectId === subjectId);
52897
54314
  if (!member) {
52898
54315
  throw new HTTPException55(500, { message: "failed to add member" });
52899
54316
  }
@@ -52904,20 +54321,32 @@ function registerWorkspaceRoutes(app, deps) {
52904
54321
  const grant = await requireAccessGrant32(c, deps, workspaceId, "members:manage");
52905
54322
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
52906
54323
  const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json());
52907
- const existing = await listWorkspaceMembers(deps.db, workspaceId);
54324
+ const existing = await listWorkspacePeople(deps, workspaceId);
52908
54325
  const current = existing.find((member2) => member2.subjectId === subjectId);
52909
54326
  if (!current) {
52910
54327
  throw new HTTPException55(404, { message: "member not found" });
52911
54328
  }
52912
- await grantWorkspaceAccess(deps.db, {
52913
- accountId: grant.accountId,
52914
- workspaceId,
54329
+ assertWorkspaceMemberUpdateAllowed({
54330
+ members: existing,
52915
54331
  subjectId,
52916
- ...current.subjectLabel ? { subjectLabel: current.subjectLabel } : {},
52917
- role: payload.role ?? current.role,
52918
- permissions: payload.permissions
54332
+ callerSubjectId: grant.subjectId,
54333
+ nextPermissions: payload.permissions
52919
54334
  });
52920
- const members = await listWorkspaceMembers(deps.db, workspaceId);
54335
+ try {
54336
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
54337
+ accountId: grant.accountId,
54338
+ workspaceId,
54339
+ actorSubjectId: grant.subjectId,
54340
+ targetSubjectId: subjectId,
54341
+ mode: "update",
54342
+ ...current.subjectLabel ? { subjectLabel: current.subjectLabel } : {},
54343
+ role: payload.role ?? current.role,
54344
+ permissions: payload.permissions
54345
+ });
54346
+ } catch (error) {
54347
+ rethrowWorkspaceMemberCandidateError(error);
54348
+ }
54349
+ const members = await listWorkspacePeople(deps, workspaceId);
52921
54350
  const member = members.find((candidate) => candidate.subjectId === subjectId);
52922
54351
  if (!member) {
52923
54352
  throw new HTTPException55(500, { message: "failed to update member" });
@@ -52929,7 +54358,11 @@ function registerWorkspaceRoutes(app, deps) {
52929
54358
  const grant = await requireAccessGrant32(c, deps, workspaceId, "members:manage");
52930
54359
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
52931
54360
  const members = await listWorkspaceMembers(deps.db, workspaceId);
52932
- assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId });
54361
+ assertWorkspaceMemberRemovable({
54362
+ members,
54363
+ subjectId,
54364
+ callerSubjectId: grant.subjectId
54365
+ });
52933
54366
  await removeWorkspaceMember(deps.db, {
52934
54367
  accountId: grant.accountId,
52935
54368
  workspaceId,
@@ -52939,6 +54372,62 @@ function registerWorkspaceRoutes(app, deps) {
52939
54372
  return c.body(null, 204);
52940
54373
  });
52941
54374
  }
54375
+ function workspaceLimit(deps) {
54376
+ if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
54377
+ return null;
54378
+ }
54379
+ return configuredStaticUsageLimits2(deps.settings).maxWorkspacesPerAccount ?? null;
54380
+ }
54381
+ async function listWorkspacePeople(deps, workspaceId) {
54382
+ const members = await listWorkspaceMembers(deps.db, workspaceId);
54383
+ const userIds = members.flatMap(
54384
+ (member) => member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : []
54385
+ );
54386
+ const profiles = await getManagedUserProfilesByIds(deps.db, userIds);
54387
+ const profileBySubject = new Map(
54388
+ profiles.map((profile) => [`user:${profile.id}`, profile.name?.trim() || profile.email])
54389
+ );
54390
+ return members.map((member) => ({
54391
+ ...member,
54392
+ subjectLabel: profileBySubject.get(member.subjectId) ?? member.subjectLabel
54393
+ }));
54394
+ }
54395
+ function rethrowWorkspaceMemberCandidateError(error) {
54396
+ const managementCode = error && typeof error === "object" && "code" in error ? error.code : void 0;
54397
+ if (managementCode === "WORKSPACE_MEMBER_ALREADY_EXISTS") {
54398
+ throw new HTTPException55(409, {
54399
+ message: "this person already has access to the workspace"
54400
+ });
54401
+ }
54402
+ if (managementCode === "WORKSPACE_MEMBER_NOT_FOUND") {
54403
+ throw new HTTPException55(404, { message: "member not found" });
54404
+ }
54405
+ if (managementCode === "WORKSPACE_MEMBER_SELF_UPDATE") {
54406
+ throw new HTTPException55(409, {
54407
+ message: "you cannot change your own workspace access"
54408
+ });
54409
+ }
54410
+ if (managementCode === "WORKSPACE_MEMBER_LAST_ADMIN") {
54411
+ throw new HTTPException55(409, {
54412
+ message: "the workspace must keep at least one administrator"
54413
+ });
54414
+ }
54415
+ const sqlState = nestedPostgresSqlState4(error);
54416
+ if (sqlState === "P0002") {
54417
+ throw new HTTPException55(404, { message: "workspace not found" });
54418
+ }
54419
+ if (sqlState === "42501") {
54420
+ throw new HTTPException55(403, {
54421
+ message: "workspace member management is not allowed"
54422
+ });
54423
+ }
54424
+ if (sqlState === "54000") {
54425
+ throw new HTTPException55(409, {
54426
+ message: "this organization has too many members to show here"
54427
+ });
54428
+ }
54429
+ throw error;
54430
+ }
52942
54431
  function normalizeAgentInstructions(value) {
52943
54432
  if (value === null) {
52944
54433
  return null;
@@ -52946,10 +54435,12 @@ function normalizeAgentInstructions(value) {
52946
54435
  const trimmed = value.trim();
52947
54436
  return trimmed.length > 0 ? trimmed : null;
52948
54437
  }
52949
- function requireAccountPermission(context, accountId, permission) {
54438
+ function requireAccountPermission2(context, accountId, permission) {
52950
54439
  const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
52951
54440
  if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
52952
- throw new HTTPException55(403, { message: `missing permission: ${permission}` });
54441
+ throw new HTTPException55(403, {
54442
+ message: `missing permission: ${permission}`
54443
+ });
52953
54444
  }
52954
54445
  }
52955
54446
 
@@ -53505,6 +54996,7 @@ function registerWorkspaceLearningRoutes(app, deps) {
53505
54996
  import { randomUUID as randomUUID10 } from "crypto";
53506
54997
  import {
53507
54998
  ActivateCompanyProfileRevisionRequest,
54999
+ CompanyProfileAgentPolicy,
53508
55000
  CompanyProfileConflictResponse,
53509
55001
  CompanyProfileDiffRequest,
53510
55002
  CompanyProfileDiffResponse,
@@ -53514,23 +55006,27 @@ import {
53514
55006
  CompanyProfileOperationReuseResponse,
53515
55007
  CompanyProfileRevision,
53516
55008
  RollbackCompanyProfileRequest,
53517
- UpdateCompanyProfileRequest
55009
+ UpdateCompanyProfileRequest,
55010
+ UpdateCompanyProfileAgentPolicyRequest
53518
55011
  } from "@opengeni/contracts";
53519
55012
  import {
53520
55013
  requireAccessGrant as requireAccessGrant35,
53521
- requireAccessGrantAuthorization as requireAccessGrantAuthorization11
55014
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization12
53522
55015
  } from "@opengeni/core";
53523
55016
  import {
53524
55017
  activateCompanyProfileRevision,
55018
+ CompanyProfileAgentPolicyError,
53525
55019
  CompanyProfileConflictError,
53526
55020
  CompanyProfileInvalidOperationError,
53527
55021
  CompanyProfileNotFoundError,
53528
55022
  CompanyProfileOperationReuseError,
53529
55023
  diffCompanyProfileRevisions,
53530
55024
  getCompanyProfileRevision,
55025
+ getCompanyProfileAgentPolicy,
53531
55026
  listCompanyProfile,
53532
55027
  rollbackCompanyProfileRevision,
53533
- updateCompanyProfile
55028
+ updateCompanyProfile,
55029
+ updateCompanyProfileAgentPolicy
53534
55030
  } from "@opengeni/db";
53535
55031
  import { HTTPException as HTTPException58 } from "hono/http-exception";
53536
55032
  import { z as z17 } from "zod";
@@ -53574,6 +55070,15 @@ function profileError(context, error) {
53574
55070
  if (error instanceof CompanyProfileInvalidOperationError) {
53575
55071
  return context.json({ code: "INVALID_COMPANY_PROFILE_OPERATION", message: error.message }, 422);
53576
55072
  }
55073
+ if (error instanceof CompanyProfileAgentPolicyError) {
55074
+ if (error.code === "authority_unavailable") {
55075
+ throw new HTTPException58(403, { message: error.message });
55076
+ }
55077
+ if (error.code === "policy_conflict" || error.code === "operation_reused") {
55078
+ throw new HTTPException58(409, { message: error.message });
55079
+ }
55080
+ throw new HTTPException58(422, { message: error.message });
55081
+ }
53577
55082
  throw error;
53578
55083
  }
53579
55084
  function revisionId(context) {
@@ -53584,6 +55089,54 @@ function revisionId(context) {
53584
55089
  }
53585
55090
  function registerCompanyProfileRoutes(app, deps) {
53586
55091
  const base = "/v1/workspaces/:workspaceId/company-profile";
55092
+ app.get(`${base}/agent-policy`, async (context) => {
55093
+ const workspaceId = context.req.param("workspaceId");
55094
+ const access = await requireAccessGrantAuthorization12(
55095
+ context,
55096
+ deps,
55097
+ workspaceId,
55098
+ "workspace:read"
55099
+ );
55100
+ requireDirectAccountAdmin(access);
55101
+ try {
55102
+ return context.json(
55103
+ CompanyProfileAgentPolicy.parse(
55104
+ await getCompanyProfileAgentPolicy(deps.db, {
55105
+ accountId: access.grant.accountId,
55106
+ workspaceId,
55107
+ actorSubjectId: access.grant.subjectId
55108
+ })
55109
+ )
55110
+ );
55111
+ } catch (error) {
55112
+ return profileError(context, error);
55113
+ }
55114
+ });
55115
+ app.patch(`${base}/agent-policy`, async (context) => {
55116
+ const workspaceId = context.req.param("workspaceId");
55117
+ const access = await requireAccessGrantAuthorization12(
55118
+ context,
55119
+ deps,
55120
+ workspaceId,
55121
+ "workspace:read"
55122
+ );
55123
+ requireDirectAccountAdmin(access);
55124
+ const request = await parseBody3(context, UpdateCompanyProfileAgentPolicyRequest);
55125
+ try {
55126
+ return context.json(
55127
+ CompanyProfileAgentPolicy.parse(
55128
+ await updateCompanyProfileAgentPolicy(deps.db, {
55129
+ accountId: access.grant.accountId,
55130
+ workspaceId,
55131
+ actorSubjectId: access.grant.subjectId,
55132
+ ...request
55133
+ })
55134
+ )
55135
+ );
55136
+ } catch (error) {
55137
+ return profileError(context, error);
55138
+ }
55139
+ });
53587
55140
  app.get(base, async (context) => {
53588
55141
  const workspaceId = context.req.param("workspaceId");
53589
55142
  const grant = await requireAccessGrant35(context, deps, workspaceId, "workspace:read");
@@ -53605,7 +55158,7 @@ function registerCompanyProfileRoutes(app, deps) {
53605
55158
  });
53606
55159
  app.put(base, async (context) => {
53607
55160
  const workspaceId = context.req.param("workspaceId");
53608
- const access = await requireAccessGrantAuthorization11(
55161
+ const access = await requireAccessGrantAuthorization12(
53609
55162
  context,
53610
55163
  deps,
53611
55164
  workspaceId,
@@ -53657,7 +55210,7 @@ function registerCompanyProfileRoutes(app, deps) {
53657
55210
  });
53658
55211
  app.post(`${base}/rollback`, async (context) => {
53659
55212
  const workspaceId = context.req.param("workspaceId");
53660
- const access = await requireAccessGrantAuthorization11(
55213
+ const access = await requireAccessGrantAuthorization12(
53661
55214
  context,
53662
55215
  deps,
53663
55216
  workspaceId,
@@ -53704,7 +55257,7 @@ function registerCompanyProfileRoutes(app, deps) {
53704
55257
  });
53705
55258
  app.post(`${base}/revisions/:revisionId/activate`, async (context) => {
53706
55259
  const workspaceId = context.req.param("workspaceId");
53707
- const access = await requireAccessGrantAuthorization11(
55260
+ const access = await requireAccessGrantAuthorization12(
53708
55261
  context,
53709
55262
  deps,
53710
55263
  workspaceId,
@@ -54628,7 +56181,7 @@ import {
54628
56181
  } from "@opengeni/contracts";
54629
56182
  import {
54630
56183
  requireAccessGrant as requireAccessGrant37,
54631
- requireAccessGrantAuthorization as requireAccessGrantAuthorization12
56184
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization13
54632
56185
  } from "@opengeni/core";
54633
56186
  import {
54634
56187
  listSlackTaskPolicy,
@@ -54687,7 +56240,7 @@ function registerSlackTaskPolicyRoutes(app, deps) {
54687
56240
  });
54688
56241
  app.put(base, async (context) => {
54689
56242
  const workspaceId = context.req.param("workspaceId");
54690
- const access = await requireAccessGrantAuthorization12(
56243
+ const access = await requireAccessGrantAuthorization13(
54691
56244
  context,
54692
56245
  deps,
54693
56246
  workspaceId,
@@ -55155,7 +56708,7 @@ import {
55155
56708
  import {
55156
56709
  hasPermission as hasPermission22,
55157
56710
  requireAccessGrant as requireAccessGrant40,
55158
- requireAccessGrantAuthorization as requireAccessGrantAuthorization13
56711
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization14
55159
56712
  } from "@opengeni/core";
55160
56713
  import {
55161
56714
  activatePreferenceRegistryRevision,
@@ -55173,7 +56726,7 @@ import {
55173
56726
  PreferenceRegistryInitiatorError,
55174
56727
  PreferenceRegistryInvalidOperationError,
55175
56728
  PreferenceRegistryNotFoundError,
55176
- PreferenceRegistryStableKeyConflictError,
56729
+ PreferenceRegistryStableKeyConflictError as PreferenceRegistryStableKeyConflictError3,
55177
56730
  rejectPreferenceRegistryProposal,
55178
56731
  supersedePreferenceRegistry
55179
56732
  } from "@opengeni/db";
@@ -55250,7 +56803,7 @@ function preferenceError(context, error) {
55250
56803
  if (error instanceof PreferenceRegistryNotFoundError) {
55251
56804
  return context.json({ code: "PREFERENCE_REGISTRY_NOT_FOUND", message: error.message }, 404);
55252
56805
  }
55253
- if (error instanceof PreferenceRegistryStableKeyConflictError) {
56806
+ if (error instanceof PreferenceRegistryStableKeyConflictError3) {
55254
56807
  return context.json({ code: error.code, message: error.message }, 409);
55255
56808
  }
55256
56809
  if (error instanceof PreferenceRegistryInvalidOperationError) {
@@ -55303,7 +56856,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55303
56856
  });
55304
56857
  app.post(`${base}/proposals`, async (context) => {
55305
56858
  const workspaceId = context.req.param("workspaceId");
55306
- const access = await requireAccessGrantAuthorization13(
56859
+ const access = await requireAccessGrantAuthorization14(
55307
56860
  context,
55308
56861
  deps,
55309
56862
  workspaceId,
@@ -55383,7 +56936,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55383
56936
  });
55384
56937
  app.post(`${base}/:preferenceId/activate`, async (context) => {
55385
56938
  const workspaceId = context.req.param("workspaceId");
55386
- const access = await requireAccessGrantAuthorization13(
56939
+ const access = await requireAccessGrantAuthorization14(
55387
56940
  context,
55388
56941
  deps,
55389
56942
  workspaceId,
@@ -55412,7 +56965,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55412
56965
  });
55413
56966
  app.post(`${base}/:preferenceId/correct`, async (context) => {
55414
56967
  const workspaceId = context.req.param("workspaceId");
55415
- const access = await requireAccessGrantAuthorization13(
56968
+ const access = await requireAccessGrantAuthorization14(
55416
56969
  context,
55417
56970
  deps,
55418
56971
  workspaceId,
@@ -55441,7 +56994,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55441
56994
  });
55442
56995
  app.post(`${base}/:preferenceId/scope`, async (context) => {
55443
56996
  const workspaceId = context.req.param("workspaceId");
55444
- const access = await requireAccessGrantAuthorization13(
56997
+ const access = await requireAccessGrantAuthorization14(
55445
56998
  context,
55446
56999
  deps,
55447
57000
  workspaceId,
@@ -55470,7 +57023,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55470
57023
  });
55471
57024
  app.post(`${base}/:preferenceId/deactivate`, async (context) => {
55472
57025
  const workspaceId = context.req.param("workspaceId");
55473
- const access = await requireAccessGrantAuthorization13(
57026
+ const access = await requireAccessGrantAuthorization14(
55474
57027
  context,
55475
57028
  deps,
55476
57029
  workspaceId,
@@ -55499,7 +57052,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55499
57052
  });
55500
57053
  app.post(`${base}/:preferenceId/supersede`, async (context) => {
55501
57054
  const workspaceId = context.req.param("workspaceId");
55502
- const access = await requireAccessGrantAuthorization13(
57055
+ const access = await requireAccessGrantAuthorization14(
55503
57056
  context,
55504
57057
  deps,
55505
57058
  workspaceId,
@@ -55528,7 +57081,7 @@ function registerPreferenceRegistryRoutes(app, deps) {
55528
57081
  });
55529
57082
  app.post(`${base}/:preferenceId/reject`, async (context) => {
55530
57083
  const workspaceId = context.req.param("workspaceId");
55531
- const access = await requireAccessGrantAuthorization13(
57084
+ const access = await requireAccessGrantAuthorization14(
55532
57085
  context,
55533
57086
  deps,
55534
57087
  workspaceId,
@@ -56980,7 +58533,8 @@ import {
56980
58533
  } from "@opengeni/contracts";
56981
58534
  import {
56982
58535
  getManagedSession as getManagedSession3,
56983
- organizationMembershipHttpStatus
58536
+ organizationMembershipHttpStatus,
58537
+ requireCanonicalLocalAccountAdministrator as requireCanonicalLocalAccountAdministrator2
56984
58538
  } from "@opengeni/core";
56985
58539
  import {
56986
58540
  acceptOrganizationInvitation,
@@ -56998,7 +58552,7 @@ import {
56998
58552
  listOrganizationAdministrationMembers,
56999
58553
  listOrganizationInvitations,
57000
58554
  listSelfOrganizationInvitations,
57001
- nestedPostgresSqlState as nestedPostgresSqlState3,
58555
+ nestedPostgresSqlState as nestedPostgresSqlState5,
57002
58556
  prepareOrganizationUserSetupDelivery,
57003
58557
  revokeOrganizationInvitation,
57004
58558
  settleOrganizationUserSetupDelivery,
@@ -57031,6 +58585,20 @@ async function requireManagedHuman(context, deps) {
57031
58585
  }
57032
58586
  return { session, subjectId: `user:${session.user.id}` };
57033
58587
  }
58588
+ async function requireOrganizationAdministrator(context, deps, organizationId2) {
58589
+ if (deps.settings.productAccessMode === "managed") {
58590
+ return await requireManagedHuman(context, deps);
58591
+ }
58592
+ if (deps.settings.productAccessMode === "local") {
58593
+ const { subjectId } = await requireCanonicalLocalAccountAdministrator2(
58594
+ context,
58595
+ deps,
58596
+ organizationId2
58597
+ );
58598
+ return { subjectId };
58599
+ }
58600
+ throw new HTTPException67(401, { message: "organization administrator session required" });
58601
+ }
57034
58602
  async function parseBody6(context, schema) {
57035
58603
  const parsed = schema.safeParse(await context.req.json().catch(() => null));
57036
58604
  if (!parsed.success) {
@@ -57046,7 +58614,7 @@ function parseId(schema, value, label) {
57046
58614
  return parsed.data;
57047
58615
  }
57048
58616
  function rethrowMembershipError(error) {
57049
- const status = organizationMembershipHttpStatus(nestedPostgresSqlState3(error));
58617
+ const status = organizationMembershipHttpStatus(nestedPostgresSqlState5(error));
57050
58618
  if (status !== null) {
57051
58619
  throw new HTTPException67(status, {
57052
58620
  message: status === 403 ? "organization administration is not authorized" : status === 404 ? "organization resource not found" : status === 409 ? "organization membership state changed; refresh and retry" : "invalid organization membership operation"
@@ -57089,7 +58657,7 @@ function registerOrganizationMembershipRoutes(app, deps) {
57089
58657
  })
57090
58658
  );
57091
58659
  } catch (error) {
57092
- if (nestedPostgresSqlState3(error) === "42501") {
58660
+ if (nestedPostgresSqlState5(error) === "42501") {
57093
58661
  throw new HTTPException67(403, {
57094
58662
  message: "organization membership is not active"
57095
58663
  });
@@ -57126,12 +58694,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57126
58694
  }
57127
58695
  });
57128
58696
  app.get("/v1/organizations/:organizationId/overview", async (context) => {
57129
- const { subjectId } = await requireManagedHuman(context, deps);
57130
58697
  const organizationId2 = parseId(
57131
58698
  OrganizationId,
57132
58699
  context.req.param("organizationId"),
57133
58700
  "organization id"
57134
58701
  );
58702
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57135
58703
  try {
57136
58704
  return context.json(
57137
58705
  OrganizationAdministrationOverview.parse(
@@ -57146,12 +58714,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57146
58714
  }
57147
58715
  });
57148
58716
  app.patch("/v1/organizations/:organizationId", async (context) => {
57149
- const { subjectId } = await requireManagedHuman(context, deps);
57150
58717
  const organizationId2 = parseId(
57151
58718
  OrganizationId,
57152
58719
  context.req.param("organizationId"),
57153
58720
  "organization id"
57154
58721
  );
58722
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57155
58723
  const payload = await parseBody6(context, UpdateOrganizationNameRequest);
57156
58724
  try {
57157
58725
  return context.json(
@@ -57168,12 +58736,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57168
58736
  }
57169
58737
  });
57170
58738
  app.patch("/v1/organizations/:organizationId/workspaces/:workspaceId", async (context) => {
57171
- const { subjectId } = await requireManagedHuman(context, deps);
57172
58739
  const organizationId2 = parseId(
57173
58740
  OrganizationId,
57174
58741
  context.req.param("organizationId"),
57175
58742
  "organization id"
57176
58743
  );
58744
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57177
58745
  const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
57178
58746
  const payload = await parseBody6(context, UpdateOrganizationWorkspaceRequest);
57179
58747
  try {
@@ -57194,12 +58762,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57194
58762
  }
57195
58763
  });
57196
58764
  app.post("/v1/organizations/:organizationId/workspaces", async (context) => {
57197
- const { subjectId } = await requireManagedHuman(context, deps);
57198
58765
  const organizationId2 = parseId(
57199
58766
  OrganizationId,
57200
58767
  context.req.param("organizationId"),
57201
58768
  "organization id"
57202
58769
  );
58770
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57203
58771
  const payload = await parseBody6(context, CreateOrganizationWorkspaceRequest);
57204
58772
  try {
57205
58773
  return context.json(
@@ -57220,12 +58788,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57220
58788
  app.patch(
57221
58789
  "/v1/organizations/:organizationId/workspaces/:workspaceId/settings",
57222
58790
  async (context) => {
57223
- const { subjectId } = await requireManagedHuman(context, deps);
57224
58791
  const organizationId2 = parseId(
57225
58792
  OrganizationId,
57226
58793
  context.req.param("organizationId"),
57227
58794
  "organization id"
57228
58795
  );
58796
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57229
58797
  const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
57230
58798
  const payload = await parseBody6(context, UpdateWorkspaceSettingsRequest2);
57231
58799
  try {
@@ -57350,7 +58918,7 @@ function registerOrganizationMembershipRoutes(app, deps) {
57350
58918
  )
57351
58919
  );
57352
58920
  } catch (error) {
57353
- if (nestedPostgresSqlState3(error) === "55000") {
58921
+ if (nestedPostgresSqlState5(error) === "55000") {
57354
58922
  throw new HTTPException67(409, {
57355
58923
  message: "private-session readiness is not activated for this organization"
57356
58924
  });
@@ -57539,12 +59107,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57539
59107
  }
57540
59108
  );
57541
59109
  app.get("/v1/organizations/:organizationId/members", async (context) => {
57542
- const { subjectId } = await requireManagedHuman(context, deps);
57543
59110
  const organizationId2 = parseId(
57544
59111
  OrganizationId,
57545
59112
  context.req.param("organizationId"),
57546
59113
  "organization id"
57547
59114
  );
59115
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57548
59116
  try {
57549
59117
  return context.json(
57550
59118
  ListOrganizationAdministrationMembersResponse.parse({
@@ -57584,12 +59152,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57584
59152
  }
57585
59153
  });
57586
59154
  app.get("/v1/organizations/:organizationId/retention-policy", async (context) => {
57587
- const { subjectId } = await requireManagedHuman(context, deps);
57588
59155
  const organizationId2 = parseId(
57589
59156
  OrganizationId,
57590
59157
  context.req.param("organizationId"),
57591
59158
  "organization id"
57592
59159
  );
59160
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57593
59161
  try {
57594
59162
  return context.json(
57595
59163
  OrganizationRetentionPolicy.parse(
@@ -57604,12 +59172,12 @@ function registerOrganizationMembershipRoutes(app, deps) {
57604
59172
  }
57605
59173
  });
57606
59174
  app.patch("/v1/organizations/:organizationId/retention-policy", async (context) => {
57607
- const { subjectId } = await requireManagedHuman(context, deps);
57608
59175
  const organizationId2 = parseId(
57609
59176
  OrganizationId,
57610
59177
  context.req.param("organizationId"),
57611
59178
  "organization id"
57612
59179
  );
59180
+ const { subjectId } = await requireOrganizationAdministrator(context, deps, organizationId2);
57613
59181
  const payload = await parseBody6(context, UpdateOrganizationRetentionPolicyRequest);
57614
59182
  try {
57615
59183
  return context.json(
@@ -57963,7 +59531,7 @@ import {
57963
59531
  completeSelfServiceOrganizationSetup,
57964
59532
  completeOrganizationUserSetup,
57965
59533
  getSelfServiceOrganizationOnboardingState,
57966
- nestedPostgresSqlState as nestedPostgresSqlState4,
59534
+ nestedPostgresSqlState as nestedPostgresSqlState6,
57967
59535
  preflightOrganizationUserSetup,
57968
59536
  previewOrganizationUserSetup
57969
59537
  } from "@opengeni/db";
@@ -58012,7 +59580,7 @@ function registerManagedOnboardingRoutes(app, deps, options = {}) {
58012
59580
  )
58013
59581
  );
58014
59582
  } catch (error) {
58015
- const sqlState = nestedPostgresSqlState4(error);
59583
+ const sqlState = nestedPostgresSqlState6(error);
58016
59584
  if (sqlState === "22023") {
58017
59585
  throw new HTTPException69(422, {
58018
59586
  message: "invalid organization setup request"
@@ -58092,7 +59660,7 @@ function registerManagedOnboardingRoutes(app, deps, options = {}) {
58092
59660
  )
58093
59661
  );
58094
59662
  } catch (error) {
58095
- const sqlState = nestedPostgresSqlState4(error);
59663
+ const sqlState = nestedPostgresSqlState6(error);
58096
59664
  if (sqlState === "22023") {
58097
59665
  throw new HTTPException69(422, {
58098
59666
  message: "invalid account setup request"
@@ -58210,6 +59778,7 @@ import {
58210
59778
  CompleteManagedAuthLoginTransactionResponse,
58211
59779
  LogoutManagedAuthLoginSlotRequest,
58212
59780
  LogoutManagedAuthSessionSetRequest,
59781
+ MANAGED_AUTH_TRANSACTION_TTL_SECONDS,
58213
59782
  MANAGED_AUTH_SESSION_SET_API_CONTRACT_HEADER,
58214
59783
  MANAGED_AUTH_SESSION_SET_API_CONTRACT_REVISION,
58215
59784
  ManagedAuthDeepLinkResolution,
@@ -58217,7 +59786,9 @@ import {
58217
59786
  ManagedAuthSessionSetProjection,
58218
59787
  ManagedAuthSessionSetErrorCode,
58219
59788
  ResolveManagedAuthDeepLinkRequest,
58220
- SelectManagedAuthLoginSlotRequest
59789
+ SelectManagedAuthLoginSlotRequest,
59790
+ StartManagedAuthSocialTransactionRequest,
59791
+ StartManagedAuthSocialTransactionResponse
58221
59792
  } from "@opengeni/contracts/managed-auth-session-sets";
58222
59793
  import { hasPermission as hasPermission23, requireSessionAuthorization as requireSessionAuthorization8 } from "@opengeni/core";
58223
59794
  import {
@@ -58260,6 +59831,13 @@ import {
58260
59831
  import { ensureManagedAccessForUser as ensureManagedAccessForUser2, getSession as getSession8 } from "@opengeni/db";
58261
59832
  import { deleteCookie as deleteCookie3, getCookie, setCookie as setCookie3 } from "hono/cookie";
58262
59833
  import { HTTPException as HTTPException70 } from "hono/http-exception";
59834
+ import { z as z24 } from "zod";
59835
+ var ManagedAuthSocialStartReceipt = z24.object({
59836
+ version: z24.literal(1),
59837
+ requestDigest: z24.string().regex(/^[0-9a-f]{64}$/u),
59838
+ url: z24.string().url().max(4096),
59839
+ stateCookie: z24.string().min(1).max(4096)
59840
+ }).strict();
58263
59841
  function registerManagedAuthSessionSetRoutes(app, deps) {
58264
59842
  const noStore = async (context, next) => {
58265
59843
  context.header("cache-control", "no-store");
@@ -58482,6 +60060,125 @@ function registerManagedAuthSessionSetRoutes(app, deps) {
58482
60060
  throwHttp(error);
58483
60061
  }
58484
60062
  });
60063
+ app.post("/v1/auth/session-set/transactions/social", async (context) => {
60064
+ const body4 = await bodyAs(context, StartManagedAuthSocialTransactionRequest);
60065
+ const { authority, actorEpoch } = await requireMutation(context, deps, body4.expectedGeneration);
60066
+ const available = requireAvailable(deps);
60067
+ if (!managedAuthSocialProviderConfigured(deps, body4.provider)) {
60068
+ throw managedAuthApiError(404, "managed_authentication_unavailable");
60069
+ }
60070
+ const transactionSecret = requireTransactionSecret(context, body4.transactionId);
60071
+ const publicBaseUrl = deps.settings.publicBaseUrl;
60072
+ if (!publicBaseUrl) {
60073
+ throw managedAuthApiError(503, "managed_authentication_unavailable", {
60074
+ retryable: true
60075
+ });
60076
+ }
60077
+ const callbackURL = new URL("/account-auth", publicBaseUrl);
60078
+ callbackURL.searchParams.set("transaction", body4.transactionId);
60079
+ callbackURL.searchParams.set("social", "complete");
60080
+ const errorCallbackURL = new URL(callbackURL);
60081
+ errorCallbackURL.searchParams.set("social", "error");
60082
+ try {
60083
+ const authorityHash = managedAuthSha256(authority);
60084
+ const transactionSecretHash = managedAuthSha256(transactionSecret);
60085
+ const requestDigest = digest(deps, {
60086
+ operation: "social_start",
60087
+ authorityHash,
60088
+ actorEpoch,
60089
+ transactionSecretHash,
60090
+ request: body4
60091
+ });
60092
+ const receiptIdentifier = `opengeni-managed-social-start:${authorityHash}:${body4.operationId}`;
60093
+ const receipt = await deps.db.transaction(async (transaction) => {
60094
+ await transaction.execute(
60095
+ sql`select pg_advisory_xact_lock(hashtextextended(${receiptIdentifier}, 0))`
60096
+ );
60097
+ const authContext = await available.managedAuth.$context;
60098
+ const stored = await authContext.internalAdapter.findVerificationValue(receiptIdentifier);
60099
+ if (stored && new Date(stored.expiresAt).getTime() > Date.now()) {
60100
+ let parsed2;
60101
+ try {
60102
+ parsed2 = ManagedAuthSocialStartReceipt.safeParse(JSON.parse(stored.value));
60103
+ } catch (error) {
60104
+ throw new ManagedAuthCompletionOutcomeUnknownError({
60105
+ cause: error
60106
+ });
60107
+ }
60108
+ if (!parsed2.success) {
60109
+ throw new ManagedAuthCompletionOutcomeUnknownError({
60110
+ cause: parsed2.error
60111
+ });
60112
+ }
60113
+ if (parsed2.data.requestDigest !== requestDigest) {
60114
+ throw new ManagedAuthSessionSetOperationReuseError();
60115
+ }
60116
+ return parsed2.data;
60117
+ }
60118
+ if (stored) {
60119
+ await authContext.internalAdapter.deleteVerificationByIdentifier(receiptIdentifier);
60120
+ }
60121
+ const result = await available.managedAuth.api.signInSocial({
60122
+ body: {
60123
+ provider: body4.provider,
60124
+ disableRedirect: true,
60125
+ callbackURL: callbackURL.toString(),
60126
+ errorCallbackURL: errorCallbackURL.toString(),
60127
+ additionalData: {
60128
+ opengeniManagedAuth: {
60129
+ version: 1,
60130
+ operationId: body4.operationId,
60131
+ provider: body4.provider,
60132
+ transactionId: body4.transactionId,
60133
+ authorityHash,
60134
+ transactionSecretHash,
60135
+ expectedGeneration: body4.expectedGeneration,
60136
+ expectedActorEpoch: actorEpoch
60137
+ }
60138
+ }
60139
+ },
60140
+ headers: isolatedManagedAuthHeaders(context.req.raw),
60141
+ returnHeaders: true
60142
+ });
60143
+ const url = validatedManagedAuthSocialAuthorizationUrl({
60144
+ provider: body4.provider,
60145
+ rawUrl: result.response?.url,
60146
+ expectedClientId: body4.provider === "google" ? deps.settings.managedAuthGoogleClientId : deps.settings.managedAuthGithubClientId,
60147
+ expectedCallbackUrl: new URL(
60148
+ `/v1/auth/callback/${body4.provider}`,
60149
+ publicBaseUrl
60150
+ ).toString()
60151
+ });
60152
+ const stateCookieName = authContext.createAuthCookie("state").name;
60153
+ const stateCookie2 = setCookieHeaders(result.headers).find(
60154
+ (cookie) => cookie.startsWith(`${stateCookieName}=`)
60155
+ );
60156
+ const parsed = ManagedAuthSocialStartReceipt.parse({
60157
+ version: 1,
60158
+ requestDigest,
60159
+ url,
60160
+ stateCookie: stateCookie2
60161
+ });
60162
+ const created = await authContext.internalAdapter.createVerificationValue({
60163
+ identifier: receiptIdentifier,
60164
+ value: JSON.stringify(parsed),
60165
+ expiresAt: new Date(Date.now() + MANAGED_AUTH_TRANSACTION_TTL_SECONDS * 1e3)
60166
+ });
60167
+ if (!created) {
60168
+ throw new ManagedAuthCompletionOutcomeUnknownError();
60169
+ }
60170
+ return parsed;
60171
+ });
60172
+ context.header("set-cookie", receipt.stateCookie, { append: true });
60173
+ return jsonWithActorEpoch(
60174
+ context,
60175
+ actorEpoch,
60176
+ StartManagedAuthSocialTransactionResponse.parse({ url: receipt.url })
60177
+ );
60178
+ } catch (error) {
60179
+ throwHttp(error);
60180
+ }
60181
+ });
58485
60182
  app.delete("/v1/auth/session-set/transactions/:transactionId", async (context) => {
58486
60183
  const body4 = await bodyAs(context, CancelManagedAuthLoginTransactionRequest);
58487
60184
  if (body4.transactionId !== context.req.param("transactionId")) invalid();
@@ -58618,6 +60315,26 @@ function registerManagedAuthSessionSetRoutes(app, deps) {
58618
60315
  );
58619
60316
  });
58620
60317
  }
60318
+ function validatedManagedAuthSocialAuthorizationUrl(input) {
60319
+ if (typeof input.rawUrl !== "string") {
60320
+ throw new ManagedAuthCompletionOutcomeUnknownError();
60321
+ }
60322
+ let url;
60323
+ try {
60324
+ url = new URL(input.rawUrl);
60325
+ } catch (error) {
60326
+ throw new ManagedAuthCompletionOutcomeUnknownError({ cause: error });
60327
+ }
60328
+ const expectedEndpoint = input.provider === "google" ? { origin: "https://accounts.google.com", pathname: "/o/oauth2/v2/auth" } : { origin: "https://github.com", pathname: "/login/oauth/authorize" };
60329
+ const exactlyOne = (name, expected) => {
60330
+ const values = url.searchParams.getAll(name);
60331
+ return values.length === 1 && values[0] !== "" && (expected === void 0 || values[0] === expected);
60332
+ };
60333
+ if (url.origin !== expectedEndpoint.origin || url.pathname !== expectedEndpoint.pathname || url.username || url.password || url.hash || !exactlyOne("client_id", input.expectedClientId) || !exactlyOne("redirect_uri", input.expectedCallbackUrl) || !exactlyOne("state")) {
60334
+ throw new ManagedAuthCompletionOutcomeUnknownError();
60335
+ }
60336
+ return input.rawUrl;
60337
+ }
58621
60338
  function requireManagedAuthProviderRouteAllowed(method, pathname) {
58622
60339
  const normalizedMethod = method.toUpperCase();
58623
60340
  const allowed2 = normalizedMethod === "POST" && (/* @__PURE__ */ new Set([
@@ -58626,16 +60343,27 @@ function requireManagedAuthProviderRouteAllowed(method, pathname) {
58626
60343
  "/v1/auth/send-verification-email",
58627
60344
  "/v1/auth/request-password-reset",
58628
60345
  "/v1/auth/reset-password"
58629
- ])).has(pathname) || normalizedMethod === "GET" && (pathname === "/v1/auth/verify-email" || pathname === "/v1/auth/error" || pathname === "/v1/auth/ok" || /^\/v1\/auth\/reset-password\/[^/]+$/.test(pathname));
60346
+ ])).has(pathname) || normalizedMethod === "GET" && (pathname === "/v1/auth/verify-email" || pathname === "/v1/auth/error" || pathname === "/v1/auth/ok" || /^\/v1\/auth\/callback\/(?:google|github)$/.test(pathname) || /^\/v1\/auth\/reset-password\/[^/]+$/.test(pathname));
58630
60347
  if (!allowed2) throw managedAuthApiError(409, "provider_route_blocked");
58631
60348
  }
60349
+ function managedAuthSocialProviderConfigured(deps, provider) {
60350
+ return provider === "google" ? Boolean(
60351
+ deps.settings.managedAuthGoogleClientId && deps.settings.managedAuthGoogleClientSecret
60352
+ ) : Boolean(
60353
+ deps.settings.managedAuthGithubClientId && deps.settings.managedAuthGithubClientSecret
60354
+ );
60355
+ }
58632
60356
  async function scrubManagedAuthProviderResponse(response, options = {}) {
58633
60357
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
58634
60358
  const headers = new Headers(response.headers);
58635
60359
  headers.set("cache-control", "no-store");
58636
60360
  headers.set("pragma", "no-cache");
58637
60361
  if (options.replacementCookies !== void 0) {
60362
+ const preserved = setCookieHeaders(response.headers).filter(
60363
+ (cookie) => options.preserveCookieNames?.some((name) => cookie.startsWith(`${name}=`))
60364
+ );
58638
60365
  headers.delete("set-cookie");
60366
+ for (const cookie of preserved) headers.append("set-cookie", cookie);
58639
60367
  for (const cookie of options.replacementCookies) headers.append("set-cookie", cookie);
58640
60368
  }
58641
60369
  let body4 = response.body;
@@ -59037,15 +60765,15 @@ import {
59037
60765
  import {
59038
60766
  issueManagedHumanUserResourceGrant,
59039
60767
  listManagedHumanUserResourceAuthorities,
59040
- requireAccessGrantAuthorization as requireAccessGrantAuthorization14,
60768
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization15,
59041
60769
  revokeManagedHumanUserResourceGrant,
59042
60770
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError8,
59043
60771
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError7,
59044
60772
  SessionTenancyManagedHumanRequiredError as SessionTenancyManagedHumanRequiredError2
59045
60773
  } from "@opengeni/core";
59046
- import { nestedPostgresSqlState as nestedPostgresSqlState5, SessionTenancyNotActivatedError as SessionTenancyNotActivatedError2 } from "@opengeni/db";
60774
+ import { nestedPostgresSqlState as nestedPostgresSqlState7, SessionTenancyNotActivatedError as SessionTenancyNotActivatedError2 } from "@opengeni/db";
59047
60775
  import { HTTPException as HTTPException71 } from "hono/http-exception";
59048
- import { z as z24 } from "zod";
60776
+ import { z as z25 } from "zod";
59049
60777
  async function body2(context, schema) {
59050
60778
  const parsed = schema.safeParse(await context.req.json().catch(() => null));
59051
60779
  if (!parsed.success) throw new HTTPException71(422, { message: "invalid user-resource request" });
@@ -59058,10 +60786,10 @@ function lifecycleError(error) {
59058
60786
  if (error instanceof SessionAuthorizationUnavailableError7) {
59059
60787
  throw new HTTPException71(503, { message: "session authorization unavailable" });
59060
60788
  }
59061
- if (nestedPostgresSqlState5(error) === "42501") {
60789
+ if (nestedPostgresSqlState7(error) === "42501") {
59062
60790
  throw new HTTPException71(403, { message: "user-resource authority denied" });
59063
60791
  }
59064
- if (nestedPostgresSqlState5(error) === "22023") {
60792
+ if (nestedPostgresSqlState7(error) === "22023") {
59065
60793
  throw new HTTPException71(422, { message: "invalid user-resource request" });
59066
60794
  }
59067
60795
  throw error;
@@ -59077,7 +60805,7 @@ function registerUserResourceAuthorityRoutes(app, deps) {
59077
60805
  });
59078
60806
  if (!query.success) throw new HTTPException71(422, { message: "explicit scope=user required" });
59079
60807
  const workspaceId = context.req.param("workspaceId");
59080
- const access = await requireAccessGrantAuthorization14(
60808
+ const access = await requireAccessGrantAuthorization15(
59081
60809
  context,
59082
60810
  deps,
59083
60811
  workspaceId,
@@ -59102,10 +60830,10 @@ function registerUserResourceAuthorityRoutes(app, deps) {
59102
60830
  });
59103
60831
  app.post(`${base}/:authorityId/grants`, async (context) => {
59104
60832
  const workspaceId = context.req.param("workspaceId");
59105
- const authorityId = z24.string().uuid().safeParse(context.req.param("authorityId"));
60833
+ const authorityId = z25.string().uuid().safeParse(context.req.param("authorityId"));
59106
60834
  if (!authorityId.success) throw new HTTPException71(422, { message: "invalid authority id" });
59107
60835
  const request = await body2(context, IssueUserResourceGrantRequest);
59108
- const access = await requireAccessGrantAuthorization14(
60836
+ const access = await requireAccessGrantAuthorization15(
59109
60837
  context,
59110
60838
  deps,
59111
60839
  workspaceId,
@@ -59132,10 +60860,10 @@ function registerUserResourceAuthorityRoutes(app, deps) {
59132
60860
  app.delete(`${base}/grants/:grantId`, async (context) => {
59133
60861
  const query = RevokeUserResourceGrantQuery.safeParse({ scope: context.req.query("scope") });
59134
60862
  if (!query.success) throw new HTTPException71(422, { message: "explicit scope=user required" });
59135
- const grantId = z24.string().uuid().safeParse(context.req.param("grantId"));
60863
+ const grantId = z25.string().uuid().safeParse(context.req.param("grantId"));
59136
60864
  if (!grantId.success) throw new HTTPException71(422, { message: "invalid grant id" });
59137
60865
  const workspaceId = context.req.param("workspaceId");
59138
- const access = await requireAccessGrantAuthorization14(
60866
+ const access = await requireAccessGrantAuthorization15(
59139
60867
  context,
59140
60868
  deps,
59141
60869
  workspaceId,
@@ -59165,15 +60893,15 @@ import {
59165
60893
  import {
59166
60894
  issueManagedHumanUserResourceGrant as issueManagedHumanUserResourceGrant2,
59167
60895
  listManagedHumanUserResourceAuthorities as listManagedHumanUserResourceAuthorities2,
59168
- requireAccessGrantAuthorization as requireAccessGrantAuthorization15,
60896
+ requireAccessGrantAuthorization as requireAccessGrantAuthorization16,
59169
60897
  revokeManagedHumanUserResourceGrant as revokeManagedHumanUserResourceGrant2,
59170
60898
  SessionAuthorizationDeniedError as SessionAuthorizationDeniedError9,
59171
60899
  SessionAuthorizationUnavailableError as SessionAuthorizationUnavailableError8,
59172
60900
  SessionTenancyManagedHumanRequiredError as SessionTenancyManagedHumanRequiredError3
59173
60901
  } from "@opengeni/core";
59174
- import { nestedPostgresSqlState as nestedPostgresSqlState6, SessionTenancyNotActivatedError as SessionTenancyNotActivatedError3 } from "@opengeni/db";
60902
+ import { nestedPostgresSqlState as nestedPostgresSqlState8, SessionTenancyNotActivatedError as SessionTenancyNotActivatedError3 } from "@opengeni/db";
59175
60903
  import { HTTPException as HTTPException73 } from "hono/http-exception";
59176
- import { z as z25 } from "zod";
60904
+ import { z as z26 } from "zod";
59177
60905
 
59178
60906
  // src/connection-authority-owner.ts
59179
60907
  import {
@@ -59213,10 +60941,10 @@ function lifecycleError2(error) {
59213
60941
  if (error instanceof SessionAuthorizationUnavailableError8) {
59214
60942
  throw new HTTPException73(503, { message: "session authorization unavailable" });
59215
60943
  }
59216
- if (nestedPostgresSqlState6(error) === "42501") {
60944
+ if (nestedPostgresSqlState8(error) === "42501") {
59217
60945
  throw new HTTPException73(403, { message: "connection authority denied" });
59218
60946
  }
59219
- if (nestedPostgresSqlState6(error) === "22023") {
60947
+ if (nestedPostgresSqlState8(error) === "22023") {
59220
60948
  throw new HTTPException73(422, { message: "invalid connection authority" });
59221
60949
  }
59222
60950
  throw error;
@@ -59231,7 +60959,7 @@ function registerConnectionAuthorityRoutes(app, deps) {
59231
60959
  });
59232
60960
  if (!query.success) throw new HTTPException73(422, { message: "explicit scope=user required" });
59233
60961
  const workspaceId = context.req.param("workspaceId");
59234
- const access = await requireAccessGrantAuthorization15(
60962
+ const access = await requireAccessGrantAuthorization16(
59235
60963
  context,
59236
60964
  deps,
59237
60965
  workspaceId,
@@ -59253,10 +60981,10 @@ function registerConnectionAuthorityRoutes(app, deps) {
59253
60981
  });
59254
60982
  app.post(`${base}/:authorityId/grants`, async (context) => {
59255
60983
  const workspaceId = context.req.param("workspaceId");
59256
- const authorityId = z25.string().uuid().safeParse(context.req.param("authorityId"));
60984
+ const authorityId = z26.string().uuid().safeParse(context.req.param("authorityId"));
59257
60985
  if (!authorityId.success) throw new HTTPException73(422, { message: "invalid authority id" });
59258
60986
  const request = await body3(context, IssueConnectionUseGrantRequest2);
59259
- const access = await requireAccessGrantAuthorization15(
60987
+ const access = await requireAccessGrantAuthorization16(
59260
60988
  context,
59261
60989
  deps,
59262
60990
  workspaceId,
@@ -59284,10 +61012,10 @@ function registerConnectionAuthorityRoutes(app, deps) {
59284
61012
  app.delete(`${base}/grants/:grantId`, async (context) => {
59285
61013
  const query = RevokeConnectionUseGrantQuery.safeParse({ scope: context.req.query("scope") });
59286
61014
  if (!query.success) throw new HTTPException73(422, { message: "explicit scope=user required" });
59287
- const grantId = z25.string().uuid().safeParse(context.req.param("grantId"));
61015
+ const grantId = z26.string().uuid().safeParse(context.req.param("grantId"));
59288
61016
  if (!grantId.success) throw new HTTPException73(422, { message: "invalid grant id" });
59289
61017
  const workspaceId = context.req.param("workspaceId");
59290
- const access = await requireAccessGrantAuthorization15(
61018
+ const access = await requireAccessGrantAuthorization16(
59291
61019
  context,
59292
61020
  deps,
59293
61021
  workspaceId,
@@ -59917,7 +61645,7 @@ import {
59917
61645
  controlHumanSessionWorkstream as controlHumanSessionWorkstream2,
59918
61646
  createSessionForRequest as createSessionForRequest2,
59919
61647
  hasPermission as hasPermission24,
59920
- requireAccessContext as requireAccessContext3,
61648
+ requireAccessContext as requireAccessContext4,
59921
61649
  requireAccessGrant as requireAccessGrant45,
59922
61650
  requireSessionAuthorizationListScope as requireSessionAuthorizationListScope3
59923
61651
  } from "@opengeni/core";
@@ -63995,7 +65723,7 @@ async function requireManagedSlackLinkHuman(c, deps) {
63995
65723
  if (c.req.header("authorization")) {
63996
65724
  throw new HTTPException74(401, { message: "managed browser sign-in required" });
63997
65725
  }
63998
- const context = await requireAccessContext3(c, deps);
65726
+ const context = await requireAccessContext4(c, deps);
63999
65727
  if (context.mode !== "managed" || !context.subjectId.startsWith("user:")) {
64000
65728
  throw new HTTPException74(403, { message: "managed browser sign-in required" });
64001
65729
  }
@@ -64495,20 +66223,96 @@ function createAppComposition(deps) {
64495
66223
  registerManagedAuthSessionSetRoutes(app, routeDeps);
64496
66224
  if (managedAuth) {
64497
66225
  app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
66226
+ const pathname = new URL(c.req.url).pathname;
66227
+ const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
64498
66228
  if (deps.settings.managedAuthSessionSetMode === "legacy") {
64499
- return await managedAuth.handler(c.req.raw);
64500
- }
64501
- requireManagedAuthProviderRouteAllowed(c.req.method, new URL(c.req.url).pathname);
64502
- const headers = new Headers(c.req.raw.headers);
64503
- headers.delete("cookie");
64504
- headers.delete("authorization");
64505
- headers.delete("x-forwarded-user");
64506
- const providerRequest = new Request(c.req.raw, { headers });
66229
+ return oauthCallbackProvider ? await runManagedAuthProvider(
66230
+ oauthCallbackProvider,
66231
+ async () => await managedAuth.handler(c.req.raw)
66232
+ ) : await managedAuth.handler(c.req.raw);
66233
+ }
66234
+ requireManagedAuthProviderRouteAllowed(c.req.method, pathname);
64507
66235
  const authority = getCookie2(c, MANAGED_AUTH_SESSION_SET_COOKIE2);
64508
- const discardProviderSession = deps.settings.managedAuthSessionSetMode === "broker" || authority !== void 0;
64509
- const providerResponse = discardProviderSession ? await runManagedAuthDiscardedProviderSession(
64510
- async () => await managedAuth.handler(providerRequest)
64511
- ) : await managedAuth.handler(providerRequest);
66236
+ let providerResponse;
66237
+ let preserveCookieNames;
66238
+ if (oauthCallbackProvider) {
66239
+ const attempt = await resolveManagedAuthOAuthAttempt(
66240
+ managedAuth,
66241
+ c.req.raw,
66242
+ oauthCallbackProvider,
66243
+ deps.settings.publicBaseUrl
66244
+ );
66245
+ if (!attempt || !authority || attempt.authorityHash !== managedAuthSha2562(authority)) {
66246
+ throw new HTTPException75(409, { message: "provider_route_blocked" });
66247
+ }
66248
+ const isolated = await isolatedManagedAuthOAuthCallbackRequest(managedAuth, c.req.raw);
66249
+ preserveCookieNames = [isolated.stateCookieName];
66250
+ const handled = await runManagedAuthAttempt(
66251
+ attempt.transactionId,
66252
+ oauthCallbackProvider,
66253
+ async () => {
66254
+ const response = await managedAuth.handler(isolated.request);
66255
+ return {
66256
+ response,
66257
+ authSessionId: currentManagedAuthCreatedSessionId()
66258
+ };
66259
+ }
66260
+ );
66261
+ providerResponse = handled.response;
66262
+ if (!handled.authSessionId && managedAuthOAuthReturnMatches(
66263
+ providerResponse.headers.get("location") ?? "",
66264
+ new URL(deps.settings.publicBaseUrl).origin,
66265
+ attempt.transactionId,
66266
+ "complete"
66267
+ )) {
66268
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl);
66269
+ location.searchParams.set("transaction", attempt.transactionId);
66270
+ location.searchParams.set("social", "error");
66271
+ providerResponse = Response.redirect(location, 302);
66272
+ } else if (handled.authSessionId) {
66273
+ try {
66274
+ await adoptManagedAuthSession({
66275
+ db: deps.db,
66276
+ adapter: managedAuthSessionAdapter,
66277
+ authority,
66278
+ authorityHash: attempt.authorityHash,
66279
+ csrfHash: managedAuthCsrfHash2(authority),
66280
+ operationId: managedAuthDerivedUuid2(
66281
+ "opengeni:managed-auth:social-completion",
66282
+ `${attempt.transactionId}:${attempt.provider}`
66283
+ ),
66284
+ requestDigest: managedAuthSecretRequestDigest2(deps.settings.betterAuthSecret, {
66285
+ operation: "social_completion",
66286
+ transactionId: attempt.transactionId,
66287
+ provider: attempt.provider,
66288
+ expectedGeneration: attempt.expectedGeneration,
66289
+ expectedActorEpoch: attempt.expectedActorEpoch
66290
+ }),
66291
+ expectedGeneration: attempt.expectedGeneration,
66292
+ expectedActorEpoch: attempt.expectedActorEpoch,
66293
+ transactionId: attempt.transactionId,
66294
+ transactionSecretHash: attempt.transactionSecretHash,
66295
+ authSessionId: handled.authSessionId,
66296
+ mode: deps.settings.managedAuthSessionSetMode
66297
+ });
66298
+ } catch {
66299
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl);
66300
+ location.searchParams.set("transaction", attempt.transactionId);
66301
+ location.searchParams.set("social", "error");
66302
+ providerResponse = Response.redirect(location, 302);
66303
+ }
66304
+ }
66305
+ } else {
66306
+ const headers = new Headers(c.req.raw.headers);
66307
+ headers.delete("cookie");
66308
+ headers.delete("authorization");
66309
+ headers.delete("x-forwarded-user");
66310
+ const providerRequest = new Request(c.req.raw, { headers });
66311
+ const discardProviderSession = deps.settings.managedAuthSessionSetMode === "broker" || authority !== void 0;
66312
+ providerResponse = discardProviderSession ? await runManagedAuthDiscardedProviderSession(
66313
+ async () => await managedAuth.handler(providerRequest)
66314
+ ) : await managedAuth.handler(providerRequest);
66315
+ }
64512
66316
  let replacementCookies;
64513
66317
  if (deps.settings.managedAuthSessionSetMode === "broker") {
64514
66318
  replacementCookies = await managedAuthSessionAdapter.createLegacySelectedSessionCookies(
@@ -64536,19 +66340,23 @@ function createAppComposition(deps) {
64536
66340
  }
64537
66341
  }
64538
66342
  }
64539
- return await scrubManagedAuthProviderResponse(providerResponse, { replacementCookies });
66343
+ return await scrubManagedAuthProviderResponse(providerResponse, {
66344
+ replacementCookies,
66345
+ preserveCookieNames
66346
+ });
64540
66347
  });
64541
66348
  }
64542
- app.get(
64543
- "/healthz",
64544
- (c) => c.json({
66349
+ app.get("/healthz", (c) => {
66350
+ const warnings = githubAppBotIdentityWarnings(deps.settings);
66351
+ return c.json({
64545
66352
  service: deps.settings.serviceName,
64546
66353
  environment: deps.settings.environment,
64547
66354
  deploymentRevision: deps.settings.deploymentRevision,
64548
66355
  ...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
66356
+ ...warnings.length > 0 ? { warnings } : {},
64549
66357
  ok: true
64550
- })
64551
- );
66358
+ });
66359
+ });
64552
66360
  app.get("/readyz", async (c) => {
64553
66361
  const result = await runReadinessChecks(readinessChecks(deps), 2e3);
64554
66362
  return c.json(result, result.ok ? 200 : 503);
@@ -64582,6 +66390,7 @@ function createAppComposition(deps) {
64582
66390
  models: configuredModels3(catalogSettings).map(projectClientModel),
64583
66391
  defaultReasoningEffort: deps.settings.openaiReasoningEffort,
64584
66392
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
66393
+ defaultSandboxBackend: deps.settings.sandboxBackend,
64585
66394
  mcpServers: deps.settings.mcpServers.map((server) => ({
64586
66395
  id: server.id,
64587
66396
  name: server.name ?? server.id
@@ -64828,6 +66637,10 @@ function createAppComposition(deps) {
64828
66637
  });
64829
66638
  return { app, routeDeps };
64830
66639
  }
66640
+ function managedAuthOAuthCallbackProvider(pathname) {
66641
+ const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
66642
+ return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
66643
+ }
64831
66644
  function mutationOutcomeUnknown(error, method) {
64832
66645
  if (method === "GET" || method === "HEAD" || method === "OPTIONS") return false;
64833
66646
  const cause = error instanceof HTTPException75 ? error.cause : error;
@@ -64909,7 +66722,11 @@ function clientAuthConfig(settings) {
64909
66722
  return {
64910
66723
  mode: "managedSession",
64911
66724
  session: "cookie",
64912
- emailVerificationRequired: settings.environment !== "local"
66725
+ emailVerificationRequired: settings.environment !== "local",
66726
+ socialProviders: [
66727
+ ...settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret ? ["google"] : [],
66728
+ ...settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret ? ["github"] : []
66729
+ ]
64913
66730
  };
64914
66731
  }
64915
66732
  if (settings.productAccessMode === "configured") {
@@ -65140,6 +66957,22 @@ var routeLabelPatterns = [
65140
66957
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
65141
66958
  label: "/v1/workspaces/:workspaceId/codex/status"
65142
66959
  },
66960
+ {
66961
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/source$/,
66962
+ label: "/v1/workspaces/:workspaceId/codex/source"
66963
+ },
66964
+ {
66965
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/(accounts|settings)$/,
66966
+ label: (match) => `/v1/organizations/:organizationId/codex/${match[1]}`
66967
+ },
66968
+ {
66969
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/connect\/(start|poll)$/,
66970
+ label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`
66971
+ },
66972
+ {
66973
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
66974
+ label: "/v1/organizations/:organizationId/codex/accounts/:accountId"
66975
+ },
65143
66976
  {
65144
66977
  pattern: /^\/v1\/workspaces\/[^/]+\/supergrok\/connect\/(start|poll)$/,
65145
66978
  label: (match) => `/v1/workspaces/:workspaceId/supergrok/connect/${match[1]}`
@@ -65290,6 +67123,10 @@ var routeLabelPatterns = [
65290
67123
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/observation$/,
65291
67124
  label: "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation"
65292
67125
  },
67126
+ {
67127
+ pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/screenshot$/,
67128
+ label: "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot"
67129
+ },
65293
67130
  {
65294
67131
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/operations\/[^/]+$/,
65295
67132
  label: "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/operations/:operationId"
@@ -65462,6 +67299,18 @@ var routeLabelPatterns = [
65462
67299
  pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
65463
67300
  label: "/v1/workspaces/:workspaceId/api-keys/:id"
65464
67301
  },
67302
+ {
67303
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys$/,
67304
+ label: "/v1/organizations/:organizationId/api-keys"
67305
+ },
67306
+ {
67307
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys\/[^/]+$/,
67308
+ label: "/v1/organizations/:organizationId/api-keys/:id"
67309
+ },
67310
+ {
67311
+ pattern: /^\/v1\/workspaces\/external$/,
67312
+ label: "/v1/workspaces/external"
67313
+ },
65465
67314
  {
65466
67315
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
65467
67316
  label: "/v1/workspaces/:workspaceId/scheduled-tasks"
@@ -65931,4 +67780,4 @@ export {
65931
67780
  withDefaultEnabledCapabilityMcpTools,
65932
67781
  workflowIdForSession3 as workflowIdForSession
65933
67782
  };
65934
- //# sourceMappingURL=chunk-QESX7HDK.js.map
67783
+ //# sourceMappingURL=chunk-XTLI3CBH.js.map