@opengeni/core 0.28.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18826 -6847
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -12,20 +12,28 @@
12
12
  // over the events bus) lives here, not in the leaf (which stays db-free).
13
13
 
14
14
  import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/config";
15
+ import { appendSessionCommandOutput } from "@opengeni/db/session-command-output";
16
+ import {
17
+ createProviderCommandRetainer,
18
+ supervisedCommandProtocolReady,
19
+ } from "@opengeni/db/retained-provider-commands";
15
20
  import {
16
21
  advanceWorkspaceGenerationForDirectRequest,
22
+ retainedProviderCommandPersistence,
17
23
  advanceWorkspaceGenerationForRetainedProcess,
18
- getEnrollment,
24
+ getLiveEnrollmentConnection,
19
25
  getRetainedProcess,
20
26
  getSandbox,
21
27
  markWarmLeaseInstanceLost,
22
28
  readActiveSandbox,
23
29
  retainWorkspaceMutationProcess,
30
+ SandboxRetainedProcessPromotionFencedError,
24
31
  retainedProcessSettlementIdentity,
25
32
  settleRetainedProcess,
26
33
  verifyDirectWorkspaceMutationSettlement,
27
34
  verifyRetainedProcessMutationSettlement,
28
35
  type Database,
36
+ type SandboxRetainedProcess,
29
37
  type SandboxWorkspaceMutationAdmission,
30
38
  } from "@opengeni/db";
31
39
  import { appendAndPublishEvents, type EventBus } from "@opengeni/events";
@@ -45,7 +53,9 @@ import {
45
53
  type RoutingRetainedProcess,
46
54
  type RoutingRetainedProcessTerminalProof,
47
55
  type RoutingSandboxOperationObserver,
56
+ type RoutingSandboxCaptureWaitObserver,
48
57
  type SelfhostedRelayConfig,
58
+ type SelfhostedConnectionBinding,
49
59
  type SelfhostedOpStreamDeps,
50
60
  } from "@opengeni/runtime/sandbox";
51
61
 
@@ -56,11 +66,59 @@ type PersistableMutationAdmission = {
56
66
  > | null;
57
67
  };
58
68
 
69
+ const retainWorkspaceProviderCommand = createProviderCommandRetainer(
70
+ retainWorkspaceMutationProcess,
71
+ (error) => (error instanceof SandboxRetainedProcessPromotionFencedError ? error.process : null),
72
+ );
73
+
74
+ type DirectRetainedProcessRoute = {
75
+ providerSessionId: number;
76
+ providerBackend: string;
77
+ providerInstanceId: string;
78
+ leaseEpoch: number;
79
+ routeKind: "home" | "active";
80
+ routeTargetId: string | null;
81
+ routeEpoch: number;
82
+ };
83
+
84
+ /** API-direct requests always use active-route authority. The default active
85
+ * pointer is represented by a null target id; it is not a home-route write. */
86
+ export function directRetainedProcessMatchesBackend(
87
+ durable: DirectRetainedProcessRoute,
88
+ process: RoutingRetainedProcess,
89
+ backend: ResolvedActiveBackend,
90
+ ): boolean {
91
+ return (
92
+ durable.providerSessionId === process.providerSessionId &&
93
+ durable.providerBackend === backend.kind &&
94
+ durable.providerInstanceId === backend.providerInstanceId &&
95
+ durable.leaseEpoch === backend.leaseEpoch &&
96
+ durable.routeKind === "active" &&
97
+ durable.routeTargetId === backend.sandboxId &&
98
+ durable.routeEpoch === backend.activeEpoch
99
+ );
100
+ }
101
+
102
+ export function retainedProcessBackgroundSettlement(
103
+ process: Pick<SandboxRetainedProcess, "state" | "exitCode" | "settlementReason">,
104
+ fallback: RoutingRetainedProcessTerminalProof,
105
+ ): { outcome: "exited" | "lost"; exitCode: number | null; reason: string } {
106
+ if (process.state === "active") {
107
+ throw new Error("Retained-process settlement returned an active durable process");
108
+ }
109
+ return {
110
+ outcome: process.state,
111
+ exitCode: process.exitCode,
112
+ reason: process.settlementReason ?? fallback.reason,
113
+ };
114
+ }
115
+
59
116
  export type ChannelARoutingServices = {
60
117
  db: Database;
61
118
  settings: Settings;
62
119
  bus?: EventBus;
63
120
  onSandboxOperation?: RoutingSandboxOperationObserver;
121
+ onSandboxCaptureWait?: RoutingSandboxCaptureWaitObserver;
64
122
  waitSignal?: AbortSignal;
65
123
  };
66
124
 
@@ -118,24 +176,31 @@ function controlRpcFactory(bus: EventBus | undefined): () => ControlRpc {
118
176
  });
119
177
  }
120
178
 
121
- async function resolveSelfhostedOpStream(
179
+ async function resolveSelfhostedConnection(
122
180
  services: ChannelARoutingServices,
123
- workspaceId: string,
181
+ access: string | { accountId: string; workspaceId: string; subjectId: string },
124
182
  sandbox: RoutableSandbox,
125
- ): Promise<SelfhostedOpStreamDeps | undefined> {
126
- if (
127
- services.settings.agentOpStreamEnabled !== true ||
128
- !services.bus?.getOpStreamConnection ||
129
- !sandbox.enrollmentId
130
- ) {
131
- return undefined;
132
- }
133
- const enrollment = await getEnrollment(services.db, workspaceId, sandbox.enrollmentId);
134
- if (enrollment?.opStream !== true) return undefined;
183
+ ): Promise<SelfhostedConnectionBinding | null> {
184
+ if (!sandbox.enrollmentId) return null;
185
+ const enrollment = await getLiveEnrollmentConnection(services.db, access, sandbox.enrollmentId);
186
+ if (!enrollment?.connectionInstanceId) return null;
187
+ const opStream: SelfhostedOpStreamDeps | undefined =
188
+ services.settings.agentOpStreamEnabled === true &&
189
+ services.bus?.getOpStreamConnection &&
190
+ enrollment.opStream === true
191
+ ? {
192
+ transport: new NatsOpStreamTransport(
193
+ async () => services.bus?.getOpStreamConnection?.() ?? null,
194
+ ),
195
+ }
196
+ : undefined;
135
197
  return {
136
- transport: new NatsOpStreamTransport(
137
- async () => services.bus?.getOpStreamConnection?.() ?? null,
138
- ),
198
+ connectionInstanceId: enrollment.connectionInstanceId,
199
+ workspaceRoot: enrollment.workspaceRoot,
200
+ ...(opStream ? { opStream } : {}),
201
+ operationResourcePolicy: enrollment.operationPolicy,
202
+ operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
203
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
139
204
  };
140
205
  }
141
206
 
@@ -157,6 +222,7 @@ export function wrapChannelABoxWithRouting(
157
222
  accountId: string;
158
223
  workspaceId: string;
159
224
  sessionId: string;
225
+ resourceSubjectId: string;
160
226
  homeLease?: {
161
227
  sandboxGroupId: string;
162
228
  leaseEpoch: number;
@@ -209,6 +275,7 @@ export function wrapChannelABoxWithRouting(
209
275
  sessionId: ids.sessionId,
210
276
  requestId: ids.directRequest.requestId,
211
277
  holderId: ids.directRequest.holderId,
278
+ initiatorSubjectId: ids.resourceSubjectId,
212
279
  sandboxGroupId: homeLease.sandboxGroupId,
213
280
  expectedEpoch: backend.leaseEpoch,
214
281
  expectedInstanceId: backend.providerInstanceId,
@@ -232,7 +299,7 @@ export function wrapChannelABoxWithRouting(
232
299
  op: string;
233
300
  backend: ResolvedActiveBackend;
234
301
  admission: unknown;
235
- outcome: "resolved" | "rejected";
302
+ outcome: "resolved" | "rejected" | "outcome_unknown";
236
303
  result?: unknown;
237
304
  retainedProcess?: RoutingRetainedProcess;
238
305
  }): Promise<void> => {
@@ -256,21 +323,29 @@ export function wrapChannelABoxWithRouting(
256
323
  ) {
257
324
  throw new Error("API-direct workspace mutation settlement lacked its bound admission");
258
325
  }
259
- if (outcome === "resolved" && retainedProcess) {
260
- await retainWorkspaceMutationProcess(db, {
326
+ if ((outcome === "resolved" || outcome === "outcome_unknown") && retainedProcess) {
327
+ await retainWorkspaceProviderCommand(db, {
261
328
  accountId: ids.accountId,
262
329
  workspaceId: ids.workspaceId,
263
330
  sessionId: ids.sessionId,
264
331
  processId: retainedProcess.id,
265
332
  providerSessionId: retainedProcess.providerSessionId,
333
+ ...(retainedProcess.providerCommand
334
+ ? { providerCommand: retainedProcess.providerCommand }
335
+ : {}),
266
336
  admissionId: exactAdmission.id,
267
337
  admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
268
338
  operation: op,
269
339
  providerBinding: boundAdmission.providerBinding ?? null,
340
+ backgroundCommand: {
341
+ commandId: retainedProcess.id,
342
+ command: op,
343
+ },
270
344
  owner: {
271
345
  kind: "direct",
272
346
  requestId: ids.directRequest.requestId,
273
347
  holderId: ids.directRequest.holderId,
348
+ initiatorSubjectId: ids.resourceSubjectId,
274
349
  sandboxGroupId: homeLease.sandboxGroupId,
275
350
  expectedEpoch: backend.leaseEpoch,
276
351
  expectedInstanceId: backend.providerInstanceId,
@@ -280,12 +355,17 @@ export function wrapChannelABoxWithRouting(
280
355
  });
281
356
  return;
282
357
  }
358
+ if (outcome === "outcome_unknown")
359
+ throw new Error(
360
+ "Outcome-unknown command settlement requires its exact retained invocation",
361
+ );
283
362
  await verifyDirectWorkspaceMutationSettlement(db, {
284
363
  accountId: ids.accountId,
285
364
  workspaceId: ids.workspaceId,
286
365
  sessionId: ids.sessionId,
287
366
  requestId: ids.directRequest.requestId,
288
367
  holderId: ids.directRequest.holderId,
368
+ initiatorSubjectId: ids.resourceSubjectId,
289
369
  sandboxGroupId: homeLease.sandboxGroupId,
290
370
  expectedEpoch: backend.leaseEpoch,
291
371
  expectedInstanceId: backend.providerInstanceId,
@@ -373,21 +453,12 @@ export function wrapChannelABoxWithRouting(
373
453
  sessionId: ids.sessionId,
374
454
  processId: process.id,
375
455
  });
376
- if (
377
- !durable ||
378
- durable.providerSessionId !== process.providerSessionId ||
379
- durable.providerBackend !== backend.kind ||
380
- durable.providerInstanceId !== backend.providerInstanceId ||
381
- durable.leaseEpoch !== backend.leaseEpoch ||
382
- durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
383
- durable.routeTargetId !== backend.sandboxId ||
384
- durable.routeEpoch !== backend.activeEpoch
385
- ) {
456
+ if (!durable || !directRetainedProcessMatchesBackend(durable, process, backend)) {
386
457
  throw new Error(
387
458
  "API retained-process settlement lost its exact durable backend identity",
388
459
  );
389
460
  }
390
- await settleRetainedProcess(db, {
461
+ const settlement = await settleRetainedProcess(db, {
391
462
  accountId: ids.accountId,
392
463
  workspaceId: ids.workspaceId,
393
464
  sessionId: ids.sessionId,
@@ -398,6 +469,11 @@ export function wrapChannelABoxWithRouting(
398
469
  reason: proof.reason,
399
470
  idleGraceMs: settings.sandboxIdleGraceMs,
400
471
  });
472
+ if (settlement.backgroundCommandEvents.length > 0 && bus) {
473
+ await bus
474
+ .publish(ids.workspaceId, ids.sessionId, settlement.backgroundCommandEvents)
475
+ .catch(() => undefined);
476
+ }
401
477
  }
402
478
  : undefined;
403
479
  const resolver = makeActiveBackendResolver({
@@ -405,10 +481,19 @@ export function wrapChannelABoxWithRouting(
405
481
  defaultBackend: established.session as RoutableBackendSession,
406
482
  defaultKind: established.backendId,
407
483
  getSandbox: async (sandboxId): Promise<RoutableSandbox | null> => {
408
- const sandbox = await getSandbox(db, ids.workspaceId, sandboxId);
484
+ const sandbox = await getSandbox(
485
+ db,
486
+ {
487
+ accountId: ids.accountId,
488
+ workspaceId: ids.workspaceId,
489
+ subjectId: ids.resourceSubjectId,
490
+ },
491
+ sandboxId,
492
+ );
409
493
  return sandbox
410
494
  ? {
411
495
  id: sandbox.id,
496
+ workspaceId: sandbox.workspaceId,
412
497
  kind: sandbox.kind,
413
498
  name: sandbox.name,
414
499
  enrollmentId: sandbox.enrollmentId,
@@ -416,8 +501,16 @@ export function wrapChannelABoxWithRouting(
416
501
  : null;
417
502
  },
418
503
  controlRpcFactory: controlRpcFactory(bus),
419
- resolveSelfhostedOpStream: (sandbox) =>
420
- resolveSelfhostedOpStream(services, ids.workspaceId, sandbox),
504
+ resolveSelfhostedConnection: (sandbox) =>
505
+ resolveSelfhostedConnection(
506
+ services,
507
+ {
508
+ accountId: ids.accountId,
509
+ workspaceId: ids.workspaceId,
510
+ subjectId: ids.resourceSubjectId,
511
+ },
512
+ sandbox,
513
+ ),
421
514
  relay: relayConfigFromSettings(settings),
422
515
  selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
423
516
  selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
@@ -444,6 +537,38 @@ export function wrapChannelABoxWithRouting(
444
537
  });
445
538
 
446
539
  const proxy = new RoutingSandboxSession({
540
+ providerSupervisionReady: async () =>
541
+ settings.modalCommandSupervisionEnabled && (await supervisedCommandProtocolReady(db)),
542
+ providerCommandHandle: (value) =>
543
+ value && typeof value === "object"
544
+ ? (value as PersistableMutationAdmission).admission?.workspaceGeneration
545
+ : undefined,
546
+ providerCommandPersistence: (process) =>
547
+ retainedProviderCommandPersistence(
548
+ db,
549
+ {
550
+ accountId: ids.accountId,
551
+ workspaceId: ids.workspaceId,
552
+ sessionId: ids.sessionId,
553
+ processId: process.id,
554
+ },
555
+ bus ? (events) => bus.publish(ids.workspaceId, ids.sessionId, events) : undefined,
556
+ ),
557
+ captureProcessOutput: async ({ process, chunkId, chunk, stream, streamFidelity }) => {
558
+ const events = await appendSessionCommandOutput(db, {
559
+ accountId: ids.accountId,
560
+ workspaceId: ids.workspaceId,
561
+ sessionId: ids.sessionId,
562
+ commandId: process.id,
563
+ chunkId,
564
+ chunk,
565
+ stream,
566
+ streamFidelity,
567
+ });
568
+ if (events.length && bus)
569
+ await bus.publish(ids.workspaceId, ids.sessionId, events).catch(() => undefined);
570
+ },
571
+ bindActiveRouteOnFirstResolve: true,
447
572
  defaultResolved: {
448
573
  session: established.session as RoutableBackendSession,
449
574
  sandboxId: null,
@@ -464,6 +589,7 @@ export function wrapChannelABoxWithRouting(
464
589
  },
465
590
  resolveActiveBackend: resolver,
466
591
  ...(services.onSandboxOperation ? { onOperation: services.onSandboxOperation } : {}),
592
+ ...(services.onSandboxCaptureWait ? { onCaptureWait: services.onSandboxCaptureWait } : {}),
467
593
  ...(beforeMutation ? { beforeMutation } : {}),
468
594
  ...(afterMutation ? { afterMutation } : {}),
469
595
  ...(beforeProcessMutation ? { beforeProcessMutation } : {}),
@@ -479,6 +605,7 @@ export function wrapChannelABoxWithRouting(
479
605
  sandboxGroupId: homeLease.sandboxGroupId,
480
606
  expectedEpoch: homeLease.leaseEpoch,
481
607
  expectedInstanceId: homeLease.instanceId,
608
+ expectedBackend: homeLease.backend,
482
609
  diagnostic: "provider_not_found_during_routed_operation",
483
610
  });
484
611
  if (marked.status === "marked" && bus) {
@@ -0,0 +1,281 @@
1
+ import { sandboxImageAllowlist, type Settings } from "@opengeni/config";
2
+ import {
3
+ resolveWorkspaceDefaultSandboxImage,
4
+ type RigVersion,
5
+ type Session,
6
+ type SandboxBackend,
7
+ type SandboxOs,
8
+ } from "@opengeni/contracts";
9
+ import { getRigVersion, getWorkspace, type Database } from "@opengeni/db";
10
+ import { resolveModalCheckpointProviderBinding } from "@opengeni/runtime/sandbox";
11
+ import {
12
+ rigProviderImageContentHash,
13
+ rigProviderImageMatchesDefinition,
14
+ rigProviderImageProviderBindingKeyHash,
15
+ } from "../rigs/provider-images";
16
+
17
+ export type ManagedSessionGroupBackend = Exclude<SandboxBackend, "none" | "selfhosted">;
18
+
19
+ /**
20
+ * Resolve the provider backend behind a session's synthetic managed group.
21
+ *
22
+ * A machine-home session keeps `selfhosted` as its durable policy while a
23
+ * non-null active pointer selects the enrolled machine. Clearing that pointer
24
+ * is an explicit switch to the deployment's managed group, so API readiness,
25
+ * viewer attachment, Channel-A, and worker turns must all select the same
26
+ * provider. Deployments configured with `none` or `selfhosted` have no managed
27
+ * fallback to expose.
28
+ */
29
+ export function managedSessionGroupBackend(
30
+ deploymentBackend: SandboxBackend,
31
+ sessionBackend: SandboxBackend,
32
+ ): ManagedSessionGroupBackend | null {
33
+ if (sessionBackend === "none") return null;
34
+ const backend = sessionBackend === "selfhosted" ? deploymentBackend : sessionBackend;
35
+ return backend === "none" || backend === "selfhosted" ? null : backend;
36
+ }
37
+
38
+ const SESSION_GROUP_MACHINE_NAMES: Record<ManagedSessionGroupBackend, string> = {
39
+ local: "this computer",
40
+ docker: "Docker",
41
+ modal: "Modal",
42
+ daytona: "Daytona",
43
+ runloop: "Runloop",
44
+ e2b: "E2B",
45
+ blaxel: "Blaxel",
46
+ cloudflare: "Cloudflare",
47
+ vercel: "Vercel",
48
+ opensandbox: "OpenSandbox",
49
+ };
50
+
51
+ /** Display name and kind for the session's own managed box. The kind is the
52
+ * real provider. Callers must not collapse local, Docker, or other providers
53
+ * into Modal. */
54
+ export function sessionGroupMachinePresentation(backend: ManagedSessionGroupBackend): {
55
+ name: string;
56
+ kind: ManagedSessionGroupBackend;
57
+ } {
58
+ return { name: SESSION_GROUP_MACHINE_NAMES[backend], kind: backend };
59
+ }
60
+
61
+ /** A machine-home row carries the machine's host OS; its managed group uses the
62
+ * platform's canonical managed-sandbox OS instead. Ordinary sessions preserve
63
+ * their explicitly selected OS. */
64
+ export function managedSessionGroupOs(
65
+ sessionBackend: SandboxBackend,
66
+ sessionOs: SandboxOs,
67
+ ): SandboxOs {
68
+ return sessionBackend === "selfhosted" ? "linux" : sessionOs;
69
+ }
70
+
71
+ /**
72
+ * Apply a workspace's selected sandbox image. The deployment keeps authority:
73
+ * only an image that is on its allowlist right now is used, so removing an
74
+ * entry returns every workspace that chose it to the deployment image. The
75
+ * lease treats the image as shared state, so a change rotates a live box
76
+ * through the normal capture-and-restore path instead of reusing it.
77
+ */
78
+ export function settingsWithWorkspaceSandboxImage(
79
+ settings: Settings,
80
+ workspaceSettings: unknown,
81
+ backend: SandboxBackend,
82
+ ): Settings {
83
+ const image = resolveWorkspaceDefaultSandboxImage(workspaceSettings);
84
+ if (!image || !sandboxImageAllowlist(settings).includes(image)) return settings;
85
+ if (backend === "docker") return { ...settings, dockerImage: image };
86
+ if (backend === "modal") {
87
+ return { ...settings, modalImageRef: image, modalImageId: undefined };
88
+ }
89
+ return settings;
90
+ }
91
+
92
+ export function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings {
93
+ // Explicit Rig image overrides are intentionally inert. Preserve this helper
94
+ // as a compatibility seam for callers and historical rows while ensuring the
95
+ // deployment-owned platform image remains authoritative.
96
+ void rigImage;
97
+ return settings;
98
+ }
99
+
100
+ export function rigProviderImageSourceImage(
101
+ settings: Settings,
102
+ backend: SandboxBackend,
103
+ ): string | null {
104
+ if (backend === "modal") return settings.modalImageId ?? settings.modalImageRef ?? null;
105
+ if (backend === "docker") return settings.dockerImage ?? null;
106
+ return null;
107
+ }
108
+
109
+ export type RigProviderImageSelectionReason =
110
+ | "selected"
111
+ | "missing"
112
+ | "provider_unsupported"
113
+ | "not_ready"
114
+ | "not_cold_boot_validated"
115
+ | "content_mismatch"
116
+ | "provider_binding_unavailable"
117
+ | "provider_binding_mismatch";
118
+
119
+ export function resolveRigProviderImageSelection(
120
+ settings: Settings,
121
+ version: RigVersion | null,
122
+ backend: SandboxBackend,
123
+ currentProviderBindingKeyHash: string | null,
124
+ ): {
125
+ settings: Settings;
126
+ reason: RigProviderImageSelectionReason;
127
+ contentHash: string | null;
128
+ imageId: string | null;
129
+ } {
130
+ if (!version) return { settings, reason: "missing", contentHash: null, imageId: null };
131
+ if (backend !== "modal") {
132
+ return { settings, reason: "provider_unsupported", contentHash: null, imageId: null };
133
+ }
134
+ const image = version.providerImages[backend];
135
+ if (!image) return { settings, reason: "missing", contentHash: null, imageId: null };
136
+ if (image.status !== "ready" || !image.imageId) {
137
+ return {
138
+ settings,
139
+ reason: "not_ready",
140
+ contentHash: image.contentHash,
141
+ imageId: image.imageId,
142
+ };
143
+ }
144
+ if (image.coldBootValidation?.version !== 1) {
145
+ return {
146
+ settings,
147
+ reason: "not_cold_boot_validated",
148
+ contentHash: image.contentHash,
149
+ imageId: null,
150
+ };
151
+ }
152
+ const sourceImage = rigProviderImageSourceImage(settings, backend);
153
+ const expectedContentHash = rigProviderImageContentHash({
154
+ backend,
155
+ sourceImage,
156
+ definition: version,
157
+ });
158
+ if (
159
+ image.sourceImage !== sourceImage ||
160
+ image.contentHash !== expectedContentHash ||
161
+ !rigProviderImageMatchesDefinition(image, version)
162
+ ) {
163
+ return {
164
+ settings,
165
+ reason: "content_mismatch",
166
+ contentHash: expectedContentHash,
167
+ imageId: null,
168
+ };
169
+ }
170
+ if (!image.providerBindingKeyHash || !currentProviderBindingKeyHash) {
171
+ return {
172
+ settings,
173
+ reason: "provider_binding_unavailable",
174
+ contentHash: expectedContentHash,
175
+ imageId: null,
176
+ };
177
+ }
178
+ if (image.providerBindingKeyHash !== currentProviderBindingKeyHash) {
179
+ return {
180
+ settings,
181
+ reason: "provider_binding_mismatch",
182
+ contentHash: expectedContentHash,
183
+ imageId: null,
184
+ };
185
+ }
186
+ return {
187
+ settings: { ...settings, modalImageId: image.imageId },
188
+ reason: "selected",
189
+ contentHash: expectedContentHash,
190
+ imageId: image.imageId,
191
+ };
192
+ }
193
+
194
+ export async function resolveRigProviderImageForRun(
195
+ settings: Settings,
196
+ version: RigVersion | null,
197
+ backend: SandboxBackend,
198
+ resolveBinding: typeof resolveModalCheckpointProviderBinding = resolveModalCheckpointProviderBinding,
199
+ ): Promise<ReturnType<typeof resolveRigProviderImageSelection>> {
200
+ const image = backend === "modal" ? version?.providerImages.modal : null;
201
+ if (image?.status !== "ready" || !image.providerBindingKeyHash) {
202
+ return resolveRigProviderImageSelection(settings, version, backend, null);
203
+ }
204
+ const structural = resolveRigProviderImageSelection(
205
+ settings,
206
+ version,
207
+ backend,
208
+ image.providerBindingKeyHash,
209
+ );
210
+ if (structural.reason !== "selected") return structural;
211
+ let currentProviderBindingKeyHash: string | null = null;
212
+ try {
213
+ const binding = await resolveBinding(settings);
214
+ currentProviderBindingKeyHash = rigProviderImageProviderBindingKeyHash(binding.key);
215
+ } catch {
216
+ // A provider-native image is an optimization. The exact logical image is
217
+ // still the correct cold-create fallback when provider identity is absent.
218
+ }
219
+ return resolveRigProviderImageSelection(
220
+ settings,
221
+ version,
222
+ backend,
223
+ currentProviderBindingKeyHash,
224
+ );
225
+ }
226
+
227
+ export async function settingsWithRigProviderImage(
228
+ settings: Settings,
229
+ version: RigVersion | null,
230
+ backend: SandboxBackend,
231
+ resolveBinding: typeof resolveModalCheckpointProviderBinding = resolveModalCheckpointProviderBinding,
232
+ ): Promise<Settings> {
233
+ return (await resolveRigProviderImageForRun(settings, version, backend, resolveBinding)).settings;
234
+ }
235
+
236
+ export type SessionSandboxRuntime = {
237
+ /** Exact logical image/rig settings used to fence the durable group lease. */
238
+ settings: Settings;
239
+ image: string | null;
240
+ rigVersion: RigVersion | null;
241
+ };
242
+
243
+ export async function resolveSessionSandboxRuntime(
244
+ db: Database,
245
+ settings: Settings,
246
+ session: Pick<Session, "workspaceId" | "sandboxBackend" | "rigId" | "rigVersionId">,
247
+ ): Promise<SessionSandboxRuntime> {
248
+ const rigVersion =
249
+ session.rigId && session.rigVersionId
250
+ ? await getRigVersion(db, session.workspaceId, session.rigId, session.rigVersionId)
251
+ : null;
252
+ if (session.rigVersionId && !rigVersion) {
253
+ throw new Error(`Frozen sandbox environment version ${session.rigVersionId} is unavailable`);
254
+ }
255
+ // Setup and checks layer on the deployment image, or on the workspace's
256
+ // allowlisted selection of one.
257
+ const logicalSettings = sandboxImageAllowlist(settings).length
258
+ ? settingsWithWorkspaceSandboxImage(
259
+ settings,
260
+ (await getWorkspace(db, session.workspaceId))?.settings,
261
+ session.sandboxBackend,
262
+ )
263
+ : settings;
264
+ return {
265
+ settings: {
266
+ ...logicalSettings,
267
+ sandboxBackend: session.sandboxBackend,
268
+ },
269
+ image: rigProviderImageSourceImage(logicalSettings, session.sandboxBackend),
270
+ rigVersion,
271
+ };
272
+ }
273
+
274
+ /** Resolve provider-native cold-create optimization only for the single CAS
275
+ * winner. Ordinary API calls never perform provider identity I/O. */
276
+ export async function providerSettingsForSessionSandboxRuntime(
277
+ runtime: SessionSandboxRuntime,
278
+ backend: SandboxBackend,
279
+ ): Promise<Settings> {
280
+ return await settingsWithRigProviderImage(runtime.settings, runtime.rigVersion, backend);
281
+ }