@opengeni/core 3.0.1 → 3.1.2

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 (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -0,0 +1,161 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type {
3
+ OrganizationPrivacySettings,
4
+ UpdateOrganizationPrivacySettingsRequest,
5
+ } from "@opengeni/contracts";
6
+ import {
7
+ getOrganizationPrivacyPreferences,
8
+ getPrivateSessionCreatePolicy,
9
+ SessionTenancyNotActivatedError,
10
+ updateOrganizationPrivacyPreferences,
11
+ type Database,
12
+ type OrganizationPrivacyPreferences,
13
+ } from "@opengeni/db";
14
+
15
+ /**
16
+ * Privacy: who can see chats, schedules and their runs. An organization's
17
+ * owners and admins choose whether Only me is allowed (the private session
18
+ * setting), the default audience for new chats and schedules, and whether
19
+ * admins see that other people's Only me schedules exist. A deployment, such
20
+ * as a product that embeds Opengeni, can only narrow those choices. See
21
+ * docs/design/privacy-audiences-2026-10-11.md.
22
+ */
23
+
24
+ export type PrivacySettings = Pick<
25
+ Settings,
26
+ | "privacyOnlyMeAllowed"
27
+ | "privacyWorkspaceDefaultRequired"
28
+ | "privacyAdminScheduleSummariesRequired"
29
+ >;
30
+
31
+ export type PrivacyDeploymentLimits = OrganizationPrivacySettings["limits"];
32
+
33
+ export function privacyDeploymentLimits(settings: PrivacySettings): PrivacyDeploymentLimits {
34
+ const onlyMeAllowed = settings.privacyOnlyMeAllowed !== false;
35
+ return {
36
+ onlyMeAllowed,
37
+ // Without Only me, Workspace is the only possible default.
38
+ defaultVisibilityLocked: !onlyMeAllowed || settings.privacyWorkspaceDefaultRequired === true,
39
+ adminsSeePrivateSchedulesLocked: settings.privacyAdminScheduleSummariesRequired === true,
40
+ };
41
+ }
42
+
43
+ export type EffectiveOrganizationPrivacy = {
44
+ defaultVisibility: "private" | "workspace";
45
+ adminsSeePrivateSchedules: boolean;
46
+ };
47
+
48
+ /** The organization's choices with the deployment's limits applied. */
49
+ export function effectiveOrganizationPrivacy(
50
+ preferences: Pick<
51
+ OrganizationPrivacyPreferences,
52
+ "defaultVisibility" | "adminsSeePrivateSchedules"
53
+ >,
54
+ limits: PrivacyDeploymentLimits,
55
+ ): EffectiveOrganizationPrivacy {
56
+ return {
57
+ defaultVisibility:
58
+ !limits.defaultVisibilityLocked && preferences.defaultVisibility === "user_private"
59
+ ? "private"
60
+ : "workspace",
61
+ adminsSeePrivateSchedules:
62
+ limits.adminsSeePrivateSchedulesLocked || preferences.adminsSeePrivateSchedules,
63
+ };
64
+ }
65
+
66
+ export async function resolveOrganizationPrivacy(
67
+ deps: { db: Database; settings: PrivacySettings },
68
+ accountId: string,
69
+ ): Promise<EffectiveOrganizationPrivacy> {
70
+ return effectiveOrganizationPrivacy(
71
+ await getOrganizationPrivacyPreferences(deps.db, accountId),
72
+ privacyDeploymentLimits(deps.settings),
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Refuse a new Only me chat or schedule in a shared workspace when this
78
+ * deployment turns Only me off. A Personal workspace is only ever its owner's,
79
+ * so it is unaffected. Existing Only me items keep their audience: narrowing
80
+ * is always allowed, widening never happens silently.
81
+ */
82
+ export async function requireDeploymentAllowsOnlyMe(
83
+ deps: { db: Database; settings: PrivacySettings },
84
+ input: { workspaceId: string; actorSubjectId: string },
85
+ ): Promise<void> {
86
+ if (privacyDeploymentLimits(deps.settings).onlyMeAllowed) return;
87
+ const policy = await getPrivateSessionCreatePolicy(deps.db, input).catch(() => null);
88
+ if (!policy?.personalWorkspace) throw new SessionTenancyNotActivatedError();
89
+ }
90
+
91
+ /** Raised when a change would go past what this deployment allows. */
92
+ export class OrganizationPrivacyLockedError extends Error {
93
+ readonly name = "OrganizationPrivacyLockedError";
94
+ }
95
+
96
+ function settingsResponse(
97
+ organizationId: string,
98
+ preferences: OrganizationPrivacyPreferences,
99
+ limits: PrivacyDeploymentLimits,
100
+ ): OrganizationPrivacySettings {
101
+ return {
102
+ organizationId,
103
+ ...effectiveOrganizationPrivacy(preferences, limits),
104
+ limits,
105
+ updatedAt: preferences.updatedAt,
106
+ };
107
+ }
108
+
109
+ export async function getOrganizationPrivacySettings(
110
+ deps: { db: Database; settings: PrivacySettings },
111
+ organizationId: string,
112
+ ): Promise<OrganizationPrivacySettings> {
113
+ return settingsResponse(
114
+ organizationId,
115
+ await getOrganizationPrivacyPreferences(deps.db, organizationId),
116
+ privacyDeploymentLimits(deps.settings),
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Change the organization's privacy choices. Only an owner or admin may, and
122
+ * never past the deployment's limits.
123
+ */
124
+ export async function updateOrganizationPrivacySettings(
125
+ deps: { db: Database; settings: PrivacySettings },
126
+ input: {
127
+ organizationId: string;
128
+ actorSubjectId: string;
129
+ request: UpdateOrganizationPrivacySettingsRequest;
130
+ },
131
+ ): Promise<OrganizationPrivacySettings> {
132
+ const limits = privacyDeploymentLimits(deps.settings);
133
+ if (input.request.defaultVisibility === "private" && limits.defaultVisibilityLocked) {
134
+ throw new OrganizationPrivacyLockedError(
135
+ limits.onlyMeAllowed
136
+ ? "New chats and schedules always start as Workspace here."
137
+ : "Only me is turned off here.",
138
+ );
139
+ }
140
+ if (input.request.adminsSeePrivateSchedules === false && limits.adminsSeePrivateSchedulesLocked) {
141
+ throw new OrganizationPrivacyLockedError(
142
+ "Admins always see that Only me schedules exist here.",
143
+ );
144
+ }
145
+ const preferences = await updateOrganizationPrivacyPreferences(deps.db, {
146
+ organizationId: input.organizationId,
147
+ actorSubjectId: input.actorSubjectId,
148
+ ...(input.request.defaultVisibility
149
+ ? {
150
+ defaultVisibility:
151
+ input.request.defaultVisibility === "private"
152
+ ? ("user_private" as const)
153
+ : ("workspace_shared" as const),
154
+ }
155
+ : {}),
156
+ ...(input.request.adminsSeePrivateSchedules !== undefined
157
+ ? { adminsSeePrivateSchedules: input.request.adminsSeePrivateSchedules }
158
+ : {}),
159
+ });
160
+ return settingsResponse(input.organizationId, preferences, limits);
161
+ }
@@ -0,0 +1,133 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import {
4
+ CapabilityCatalogItem,
5
+ McpServerConnectionRef,
6
+ stableJson,
7
+ type AccessGrant,
8
+ type ConnectionMetadata,
9
+ } from "@opengeni/contracts";
10
+ import type { ConnectOwnership, PreparedMcpSetup } from "@opengeni/contracts/connect";
11
+ import type { Settings } from "@opengeni/config";
12
+ import { getCapabilityCatalogItem, type Database } from "@opengeni/db";
13
+ import { HTTPException } from "hono/http-exception";
14
+ import {
15
+ createCatalogItem,
16
+ prepareCapabilityEnable,
17
+ validateMcpCapabilityConnection,
18
+ type McpCapabilityProbe,
19
+ } from "../domain/capabilities";
20
+
21
+ /** Initialize/list tools before committing anything. Secret headers stay in this
22
+ * invocation; the installation contains only an ordinary native connection ref.
23
+ * The caller commits the connection and this installation in one authorized
24
+ * Connect operation, so retries cannot leave an orphan credential or catalog. */
25
+ export async function prepareMcpConnectionActivation(input: {
26
+ grant: AccessGrant;
27
+ settings: Settings;
28
+ setup: PreparedMcpSetup;
29
+ ownership: ConnectOwnership;
30
+ headers: Record<string, string>;
31
+ probe?: McpCapabilityProbe;
32
+ }) {
33
+ const { grant, settings, ownership } = input;
34
+ const setup = structuredClone(input.setup);
35
+ const endpoint = new URL(setup.endpointUrl).toString();
36
+ const key = createHash("sha256")
37
+ .update(stableJson({ endpoint, ownership }))
38
+ .digest("hex")
39
+ .slice(0, 32);
40
+ const capabilityId = `mcp:custom-${key}`;
41
+ const item = CapabilityCatalogItem.parse({
42
+ id: capabilityId,
43
+ kind: "mcp",
44
+ source: "manual",
45
+ name: setup.name,
46
+ endpointUrl: endpoint,
47
+ authModel: "native_connection",
48
+ runtime: { available: true, mcpServerId: `custom-${key}` },
49
+ metadata: { mcpServerId: `custom-${key}` },
50
+ });
51
+ const connectivity = await validateMcpCapabilityConnection(
52
+ item,
53
+ input.probe,
54
+ { ...input.headers },
55
+ settings,
56
+ );
57
+ return {
58
+ capabilityId,
59
+ commit: async (db: Database, connection: ConnectionMetadata) => {
60
+ const existing = await getCapabilityCatalogItem(db, grant.workspaceId, capabilityId);
61
+ if (existing && (existing.kind !== "mcp" || existing.endpointUrl !== endpoint))
62
+ throw new HTTPException(409, { message: "MCP catalog definition changed; reload setup" });
63
+ if (!existing)
64
+ await createCatalogItem({
65
+ db,
66
+ accountId: grant.accountId,
67
+ workspaceId: grant.workspaceId,
68
+ payload: {
69
+ id: capabilityId,
70
+ kind: "mcp",
71
+ source: "manual",
72
+ name: setup.name,
73
+ endpointUrl: endpoint,
74
+ authModel: "native_connection",
75
+ category: "custom",
76
+ tags: [],
77
+ metadata: item.metadata,
78
+ },
79
+ });
80
+ const connectionRef: McpServerConnectionRef = {
81
+ providerDomain: new URL(endpoint).hostname,
82
+ subjectScope: ownership === "personal" ? "subject" : "workspace",
83
+ accountSelection: "all_eligible",
84
+ kind: "api_key",
85
+ resource: endpoint,
86
+ };
87
+ const prepared = await prepareCapabilityEnable({
88
+ db,
89
+ grant,
90
+ accountId: grant.accountId,
91
+ workspaceId: grant.workspaceId,
92
+ settings,
93
+ capabilityId,
94
+ payload: {
95
+ onlyIfUninstalled: true,
96
+ config: {},
97
+ headers: {},
98
+ metadata: {},
99
+ connectionRef,
100
+ },
101
+ verifiedConnection: {
102
+ id: connection.id,
103
+ version: connection.version,
104
+ endpointUrl: endpoint,
105
+ connectivity,
106
+ },
107
+ });
108
+ const installation = await prepared.commit(db);
109
+ const savedRef = McpServerConnectionRef.safeParse(installation.config.connectionRef);
110
+ const retainedExactPin =
111
+ savedRef.success &&
112
+ savedRef.data.authoritySource !== "host" &&
113
+ savedRef.data.connectionId === connection.id &&
114
+ savedRef.data.providerDomain === connectionRef.providerDomain &&
115
+ (savedRef.data.subjectScope ?? "workspace") === connectionRef.subjectScope &&
116
+ (savedRef.data.kind ?? connection.kind) === connection.kind &&
117
+ (!savedRef.data.resource || savedRef.data.resource === endpoint);
118
+ // onlyIfUninstalled preserves existing account choices. Do not report a
119
+ // newly saved account as usable when a different binding was preserved;
120
+ // throw inside Connect's transaction so its credential also rolls back.
121
+ // Reconnecting the exact pinned account keeps the pin and its restrictions.
122
+ if (
123
+ installation.status !== "active" ||
124
+ (!retainedExactPin && !isDeepStrictEqual(installation.config.connectionRef, connectionRef))
125
+ ) {
126
+ throw new HTTPException(409, {
127
+ message: "MCP account selection changed; review the existing connection before setup",
128
+ });
129
+ }
130
+ return installation;
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,78 @@
1
+ import type { SandboxRecoveryRequest } from "@opengeni/contracts";
2
+ import { consentPublicSandboxRecovery, readPublicSandboxRecovery } from "@opengeni/db";
3
+ import { requirePermission, type AccessGrantAuthorization } from "../access";
4
+ import type { AppDependencies } from "../dependencies";
5
+ import { requireSessionAuthorization } from "../session-authorization";
6
+ import { requireCanonicalManagedHuman } from "./session-tenancy";
7
+
8
+ type Dependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
9
+
10
+ /** The read-only projection also serves the exact built-in local human. Its
11
+ * provenance stamp is set only by the local resolver, never a bearer claim. */
12
+ export function requireSandboxRecoveryPreviewHuman(
13
+ authorization: AccessGrantAuthorization,
14
+ workspaceId: string,
15
+ ): void {
16
+ if (
17
+ authorization.canonicalLocalHumanSession &&
18
+ authorization.contextIntegrity &&
19
+ authorization.authenticatedSubjectId === authorization.grant.subjectId &&
20
+ authorization.grant.workspaceId === workspaceId &&
21
+ authorization.grant.subjectId === "dev" &&
22
+ authorization.grant.principalKind === "human_session" &&
23
+ authorization.grant.metadata?.delegated !== true &&
24
+ !authorization.grant.serviceInitiator
25
+ )
26
+ return;
27
+ requireCanonicalManagedHuman(authorization, workspaceId);
28
+ }
29
+
30
+ async function authorize(
31
+ deps: Dependencies,
32
+ authorization: AccessGrantAuthorization,
33
+ workspaceId: string,
34
+ sessionId: string,
35
+ intent: "preview" | "consent",
36
+ ) {
37
+ // Cookie-verification stamp, never principal-shape inference or an actor from
38
+ // the body. The local human may only read; agents, API keys and delegated
39
+ // humans cannot accept data loss or broaden the consent mutation.
40
+ if (intent === "consent") requireCanonicalManagedHuman(authorization, workspaceId);
41
+ else requireSandboxRecoveryPreviewHuman(authorization, workspaceId);
42
+ requirePermission(authorization.grant, "sessions:read");
43
+ requirePermission(authorization.grant, "sessions:control");
44
+ await requireSessionAuthorization(deps, authorization.grant, {
45
+ sessionId,
46
+ operation: "session.control",
47
+ surface: "core",
48
+ });
49
+ return {
50
+ accountId: authorization.grant.accountId,
51
+ workspaceId,
52
+ sessionId,
53
+ subjectId: authorization.grant.subjectId,
54
+ };
55
+ }
56
+
57
+ export async function getManagedHumanSandboxRecovery(
58
+ deps: Dependencies,
59
+ authorization: AccessGrantAuthorization,
60
+ workspaceId: string,
61
+ sessionId: string,
62
+ ) {
63
+ return readPublicSandboxRecovery(
64
+ deps.db,
65
+ await authorize(deps, authorization, workspaceId, sessionId, "preview"),
66
+ );
67
+ }
68
+
69
+ export async function consentManagedHumanSandboxRecovery(
70
+ deps: Dependencies,
71
+ authorization: AccessGrantAuthorization,
72
+ workspaceId: string,
73
+ sessionId: string,
74
+ request: SandboxRecoveryRequest,
75
+ ) {
76
+ const scope = await authorize(deps, authorization, workspaceId, sessionId, "consent");
77
+ return consentPublicSandboxRecovery(deps.db, { ...scope, request });
78
+ }
@@ -17,7 +17,10 @@ import type {
17
17
  WorkspaceInferenceControlResponse,
18
18
  } from "@opengeni/contracts";
19
19
  import { DraftTimelineAnnotations } from "@opengeni/contracts";
20
+ import type { Settings } from "@opengeni/config";
21
+ import { HTTPException } from "hono/http-exception";
20
22
  import {
23
+ checkWorkspaceAllowance,
21
24
  deleteSessionQueueItemInTransaction,
22
25
  editQueuedTurnInTransaction,
23
26
  getComposerDraftInTransaction,
@@ -25,6 +28,8 @@ import {
25
28
  getSessionEvent,
26
29
  getWorkspaceControlEvent,
27
30
  getSessionQueueSnapshot,
31
+ frozenInitiatorForCommandActor,
32
+ isCodexBilledTurn,
28
33
  moveQueuedTurnInTransaction,
29
34
  mutateSessionControlInTransaction,
30
35
  mutateWorkspaceControlInTransaction,
@@ -53,6 +58,9 @@ import {
53
58
  import type { SessionWorkflowClient } from "../dependencies";
54
59
  import { normalizeResources } from "../domain/resources";
55
60
  import { validateDraftTimelineAnnotations } from "../domain/timeline-annotations";
61
+ import { initiatingHumanForAllowance } from "../domain/sessions";
62
+ import { coreRoutedSubscriptionFunding, modelFundingForAdmission } from "../billing/limits";
63
+ import { resolveWorkspaceCatalogSettings } from "../model-catalog";
56
64
  import {
57
65
  requireSessionAuthorization,
58
66
  type ResolvedSessionAuthorization,
@@ -148,6 +156,63 @@ function agentActor(context: AgentSessionCommandContext) {
148
156
  };
149
157
  }
150
158
 
159
+ /**
160
+ * Called inside the command's exact live-attempt fence, after receipt recovery
161
+ * but before any fresh work or Steer interruption. Attribution comes from the
162
+ * caller's accepted turn, never the target creator or request subject.
163
+ */
164
+ async function assertFreshAgentCommandAllowance(
165
+ tx: SessionActivityDatabase,
166
+ deps: { settings?: Settings },
167
+ context: AgentSessionCommandContext,
168
+ targetSessionId: string,
169
+ ): Promise<void> {
170
+ const target = await getSession(tx, context.workspaceId, targetSessionId);
171
+ if (!target || target.accountId !== context.accountId) {
172
+ throw new HTTPException(404, { message: "Target session not found" });
173
+ }
174
+ let fundedWithoutCredits = false;
175
+ const frozen = await frozenInitiatorForCommandActor(tx, context.workspaceId, agentActor(context));
176
+ if (deps.settings) {
177
+ const { settings } = await resolveWorkspaceCatalogSettings(tx, deps.settings, {
178
+ accountId: target.accountId,
179
+ workspaceId: context.workspaceId,
180
+ retainedProductModelId: target.model,
181
+ });
182
+ const codexBilled = await isCodexBilledTurn({
183
+ db: tx,
184
+ settings,
185
+ workspaceId: context.workspaceId,
186
+ model: target.model,
187
+ accountId: target.accountId,
188
+ subjectId: initiatingHumanForAllowance(frozen),
189
+ });
190
+ fundedWithoutCredits = modelFundingForAdmission(
191
+ settings,
192
+ target.model,
193
+ codexBilled,
194
+ await coreRoutedSubscriptionFunding(tx, settings, {
195
+ accountId: target.accountId,
196
+ workspaceId: context.workspaceId,
197
+ model: target.model,
198
+ subjectId: initiatingHumanForAllowance(frozen),
199
+ }),
200
+ ).fundedWithoutCredits;
201
+ }
202
+ const refusal = await checkWorkspaceAllowance(tx, {
203
+ accountId: target.accountId,
204
+ workspaceId: context.workspaceId,
205
+ subjectId: initiatingHumanForAllowance(frozen),
206
+ ...(fundedWithoutCredits ? { fundedWithoutCredits: true } : {}),
207
+ });
208
+ if (refusal) {
209
+ throw new HTTPException(402, {
210
+ message: refusal.message,
211
+ cause: { allowed: false, ...refusal },
212
+ });
213
+ }
214
+ }
215
+
151
216
  /**
152
217
  * Retry only one operation-keyed session command transaction. Callers keep
153
218
  * event publication and Temporal wake delivery after this returns, so a
@@ -276,7 +341,8 @@ async function wakeSessionCommand(
276
341
  /**
277
342
  * Nudge the one bounded dispatcher after a set-based control transaction. The
278
343
  * API never materializes descendant session ids; Postgres remains the complete
279
- * wake ledger and the 10-second Schedule repairs a lost immediate trigger.
344
+ * wake ledger and the control worker's 10-second repair cadence recovers a
345
+ * lost immediate trigger.
280
346
  */
281
347
  async function requestControlWakeDispatch(
282
348
  deps: {
@@ -334,6 +400,7 @@ async function publishWorkspaceControlEvent(
334
400
  export async function sendAgentSessionMessage(
335
401
  deps: {
336
402
  db: Database;
403
+ settings?: Settings;
337
404
  bus: EventBus;
338
405
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
339
406
  sessionAuthorization?: SessionAuthorizationPort | null;
@@ -356,6 +423,8 @@ export async function sendAgentSessionMessage(
356
423
  actor: agentActor(context),
357
424
  operationKey: input.idempotencyKey,
358
425
  text: input.text,
426
+ assertFreshAdmission: (scoped) =>
427
+ assertFreshAgentCommandAllowance(scoped, deps, context, input.targetSessionId),
359
428
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
360
429
  }),
361
430
  },
@@ -400,6 +469,7 @@ export async function sendAgentSessionMessage(
400
469
  export async function steerAgentSession(
401
470
  deps: {
402
471
  db: Database;
472
+ settings?: Settings;
403
473
  bus: EventBus;
404
474
  workflowClient: Pick<SessionWorkflowClient, "wakeSessionWorkflow">;
405
475
  sessionAuthorization?: SessionAuthorizationPort | null;
@@ -426,6 +496,8 @@ export async function steerAgentSession(
426
496
  actor: agentActor(context),
427
497
  operationKey: input.idempotencyKey,
428
498
  instruction: input.instruction,
499
+ assertFreshAdmission: (scoped) =>
500
+ assertFreshAgentCommandAllowance(scoped, deps, context, input.targetSessionId),
429
501
  controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
430
502
  }),
431
503
  },
@@ -14,6 +14,7 @@ import { HTTPException } from "hono/http-exception";
14
14
  import type { Settings } from "@opengeni/config";
15
15
  import {
16
16
  requirePermission,
17
+ hasVerifiedOwningUserAuthorization,
17
18
  requireResolvedAccessGrantAuthorization,
18
19
  type AccessGrantAuthorization,
19
20
  } from "../access";
@@ -24,9 +25,14 @@ import {
24
25
  SessionAuthorizationUnavailableError,
25
26
  type SessionAuthorizationDependencies,
26
27
  } from "../session-authorization";
27
- import { requireVariableSetEncryption } from "../domain/environments";
28
+ import { requireEnvironmentEncryption } from "../domain/encryption-key";
28
29
  import { normalizedSessionMcpCredentialHeaders } from "../domain/sessions";
29
30
  import { externalContinuationCommitAuthorizer } from "./external-continuation";
31
+ import {
32
+ listOwnConnectionAccountsForGrant,
33
+ personalConnectionDelegationSourceForGrant,
34
+ } from "../domain/personal-connection-delegations";
35
+ import { nativeSessionConnectionReplacement } from "../domain/session-native-connection-replacement";
30
36
 
31
37
  /** HMAC domains separate request identity from key availability. The key tag
32
38
  * carries no request material and neither fingerprint is returned publicly. */
@@ -70,7 +76,7 @@ export async function rotateSessionMcpCredentialsForRequest(
70
76
  const parsed = RotateSessionMcpCredentialsRequest.safeParse(raw);
71
77
  if (!parsed.success)
72
78
  throw new HTTPException(422, { message: "invalid credential rotation request" });
73
- const encryptionKey = requireVariableSetEncryption(deps.settings);
79
+ const encryptionKey = requireEnvironmentEncryption(deps.settings);
74
80
  let request: RotateSessionMcpCredentialsRequest;
75
81
  try {
76
82
  request = {
@@ -80,11 +86,20 @@ export async function rotateSessionMcpCredentialsForRequest(
80
86
  id: update.id,
81
87
  expectedCredentialVersion: update.expectedCredentialVersion,
82
88
  expectedServerUrl: update.expectedServerUrl,
83
- headers: Object.fromEntries(
84
- Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
85
- .map(([name, value]) => [name.toLowerCase(), value] as const)
86
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
87
- ),
89
+ ...("nativeConnectionId" in update
90
+ ? {
91
+ nativeConnectionId: update.nativeConnectionId,
92
+ ...(update.replacementServerUrl
93
+ ? { replacementServerUrl: update.replacementServerUrl }
94
+ : {}),
95
+ }
96
+ : {
97
+ headers: Object.fromEntries(
98
+ Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
99
+ .map(([name, value]) => [name.toLowerCase(), value] as const)
100
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
101
+ ),
102
+ }),
88
103
  }))
89
104
  .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
90
105
  };
@@ -102,20 +117,45 @@ export async function rotateSessionMcpCredentialsForRequest(
102
117
  actorType: grant.principalKind === "service" ? ("service" as const) : ("human" as const),
103
118
  };
104
119
  const externalAuthorize = externalContinuationCommitAuthorizer(authorization);
120
+ let authorizedGrant = grant;
105
121
  try {
106
122
  return await rotateSessionMcpCredentialsAtomically(deps.db, {
107
123
  ...scope,
108
124
  operationKey: request.operationKey,
109
125
  ...sessionMcpRotationFingerprint(encryptionKey, scope, request),
110
- updates: request.updates.map(({ headers, ...update }) => ({
111
- ...update,
112
- headersEncrypted: Object.fromEntries(
113
- Object.entries(headers).map(([name, value]) => [
114
- name,
115
- encryptVariableSetValue(encryptionKey, value),
116
- ]),
117
- ),
118
- })),
126
+ updates: request.updates.map((update) =>
127
+ "nativeConnectionId" in update
128
+ ? update
129
+ : {
130
+ id: update.id,
131
+ expectedCredentialVersion: update.expectedCredentialVersion,
132
+ expectedServerUrl: update.expectedServerUrl,
133
+ headersEncrypted: Object.fromEntries(
134
+ Object.entries(update.headers).map(([name, value]) => [
135
+ name,
136
+ encryptVariableSetValue(encryptionKey, value),
137
+ ]),
138
+ ),
139
+ },
140
+ ),
141
+ resolveNativeConnection: async (tx, server, nativeConnectionId, replacementServerUrl) => {
142
+ const connections = await listOwnConnectionAccountsForGrant(tx, authorizedGrant);
143
+ const source = personalConnectionDelegationSourceForGrant(authorizedGrant);
144
+ try {
145
+ return nativeSessionConnectionReplacement({
146
+ accountId: grant.accountId,
147
+ workspaceId: grant.workspaceId,
148
+ subjectId: source.kind === "subject" ? source.subjectId : null,
149
+ serverUrl: server.url,
150
+ ...(replacementServerUrl ? { replacementServerUrl } : {}),
151
+ currentRef: server.connectionRef,
152
+ nativeConnectionId,
153
+ connections,
154
+ });
155
+ } catch {
156
+ throw new SessionMcpCredentialRotationError("connection_unavailable");
157
+ }
158
+ },
119
159
  authorize: async (tx) => {
120
160
  await externalAuthorize?.(tx);
121
161
  const fresh = await reauthorize(tx);
@@ -133,7 +173,7 @@ export async function rotateSessionMcpCredentialsForRequest(
133
173
  const live = await getWorkspaceGrant(tx, fresh.subjectId, fresh.workspaceId);
134
174
  if (live) {
135
175
  requireRotationGrant(live);
136
- } else if (!authorization.canonicalManagedHumanSession) {
176
+ } else if (!hasVerifiedOwningUserAuthorization(authorization)) {
137
177
  throw new HTTPException(403, { message: "credential rotation authority changed" });
138
178
  }
139
179
  if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, scope))) {
@@ -145,6 +185,7 @@ export async function rotateSessionMcpCredentialsForRequest(
145
185
  operation: "session.mcp.credentials.rotate",
146
186
  surface: "http",
147
187
  });
188
+ authorizedGrant = fresh;
148
189
  },
149
190
  });
150
191
  } catch (error) {
@@ -162,7 +203,9 @@ export async function rotateSessionMcpCredentialsForRequest(
162
203
  ? 403
163
204
  : error.code === "not_found"
164
205
  ? 404
165
- : error.code === "invalid_request" || error.code === "brokered_server"
206
+ : error.code === "invalid_request" ||
207
+ error.code === "brokered_server" ||
208
+ error.code === "connection_unavailable"
166
209
  ? 422
167
210
  : 409;
168
211
  throw new HTTPException(status, { message: `credential_rotation_${error.code}` });