@opengeni/core 0.28.1 → 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 +18812 -6833
  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
@@ -1,6 +1,6 @@
1
1
  // apps/api/src/sandbox/fleet.ts — the FLEET service backing the fleet MCP tools
2
2
  // (M7): list / attach / swap / run_on / provision over the heterogeneous fleet
3
- // (the session's Modal group box + the workspace's enrolled selfhosted machines).
3
+ // (the session's managed group box + the workspace's enrolled selfhosted machines).
4
4
  //
5
5
  // Each operation is workspace-scoped (the caller's grant) and, for the
6
6
  // session-pointer mutations (attach/swap), session-scoped (the worker-signed
@@ -12,12 +12,17 @@
12
12
  // single op WITHOUT touching the active pointer.
13
13
 
14
14
  import type { Settings } from "@opengeni/config";
15
+ import type { SandboxBackend } from "@opengeni/contracts";
15
16
  import {
17
+ authorizePersonalMachineForAttempt,
16
18
  getEnrollment,
19
+ getLiveEnrollmentConnection,
17
20
  getSandbox,
21
+ listEnrollments,
18
22
  listSandboxes,
19
23
  readActiveSandbox,
20
24
  readLease,
25
+ resolvePersonalMachineConnectionForAttempt,
21
26
  requireSession,
22
27
  setActiveSandbox,
23
28
  type Database,
@@ -30,14 +35,18 @@ import {
30
35
  NatsOpStreamTransport,
31
36
  selfhostedLiveness,
32
37
  SelfhostedSession,
38
+ resolveConnectedMachineWorkspaceRoot,
33
39
  swapTargetEstablishability,
34
40
  type BackendUnresolvableCode,
35
41
  type ControlRpc,
36
42
  type NatsRequestConnection,
37
43
  type SelfhostedRelayConfig,
38
44
  type SelfhostedOpStreamDeps,
45
+ type SelfhostedOperationAdmission,
46
+ type SelfhostedOperationResourcePolicy,
39
47
  } from "@opengeni/runtime/sandbox";
40
48
  import { relayConfigFromSettings } from "./routing";
49
+ import { managedSessionGroupBackend, sessionGroupMachinePresentation } from "./runtime-settings";
41
50
 
42
51
  export type FleetServices = {
43
52
  db: Database;
@@ -57,11 +66,18 @@ export type FleetReadinessHold = {
57
66
  export type FleetContext = {
58
67
  accountId: string;
59
68
  workspaceId: string;
69
+ subjectId?: string;
70
+ attemptAuthority?: {
71
+ turnId: string;
72
+ attemptId: string;
73
+ executionGeneration: number;
74
+ initiatingHumanSubjectId: string;
75
+ };
60
76
  /** The calling session (the pointer the attach/swap mutates + whose group box
61
77
  * is the default fleet member). */
62
78
  sessionId: string;
63
- /** The session's own group sandbox backend (modal/selfhosted/…). */
64
- sessionBackend: string;
79
+ /** The session's durable home-compute policy. */
80
+ sessionBackend: SandboxBackend;
65
81
  /** The session's own group sandbox id (the lease group). */
66
82
  sessionGroupId: string;
67
83
  };
@@ -78,12 +94,20 @@ export type FleetContext = {
78
94
  */
79
95
  export async function buildFleetContextForSession(
80
96
  deps: { db: Database },
81
- ctx: { accountId: string; workspaceId: string; sessionId: string },
97
+ ctx: {
98
+ accountId: string;
99
+ workspaceId: string;
100
+ sessionId: string;
101
+ subjectId?: string;
102
+ attemptAuthority?: FleetContext["attemptAuthority"];
103
+ },
82
104
  ): Promise<FleetContext> {
83
105
  const session = await requireSession(deps.db, ctx.workspaceId, ctx.sessionId);
84
106
  return {
85
107
  accountId: ctx.accountId,
86
108
  workspaceId: ctx.workspaceId,
109
+ ...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
110
+ ...(ctx.attemptAuthority ? { attemptAuthority: ctx.attemptAuthority } : {}),
87
111
  sessionId: ctx.sessionId,
88
112
  sessionBackend: session.sandboxBackend,
89
113
  sessionGroupId: session.sandboxGroupId,
@@ -98,14 +122,15 @@ export type FleetOperationAvailability = "ready" | "wakeable" | "recovering" | "
98
122
  * A fleet member as the agent + the dock see it (the M8b/M9 UI seam — the
99
123
  * `sandboxes_list` response entry the dock renders). STABLE shape: the dock keys
100
124
  * on `id`, renders `name`/`kind`/`liveness`, and marks `active`. The session's own
101
- * Modal group box is a synthetic entry with `id: groupId`, `kind: "modal"`, and a
102
- * null `enrollmentId`; an enrolled machine carries its sandbox + enrollment ids.
125
+ * managed group box is a synthetic entry with `id: groupId`, its actual
126
+ * provisioned backend kind, and a null `enrollmentId`; an enrolled machine
127
+ * carries its sandbox + enrollment ids.
103
128
  */
104
129
  export type FleetSandboxEntry = {
105
130
  /** The sandbox id used as the attach/swap/run_on `target`. For the session's
106
131
  * own group box this is the group id (a null active pointer == this box). */
107
132
  id: string;
108
- kind: "modal" | "selfhosted";
133
+ kind: Exclude<SandboxBackend, "none">;
109
134
  name: string;
110
135
  liveness: FleetLiveness;
111
136
  /** True for the session's currently-active sandbox (the routing target). */
@@ -165,6 +190,7 @@ export type FleetSwapResult = {
165
190
  reason?: string;
166
191
  code?:
167
192
  | BackendUnresolvableCode
193
+ | "invalid_working_directory"
168
194
  | "concurrent_swap"
169
195
  | "recovery_in_progress"
170
196
  | "recovery_degraded"
@@ -191,13 +217,22 @@ async function probeEnrollment(
191
217
  services: FleetServices,
192
218
  workspaceId: string,
193
219
  enrollment: EnrollmentRecord,
194
- ): Promise<{ liveness: FleetLiveness; consented: boolean; hasDisplay: boolean }> {
220
+ liveConnection: EnrollmentRecord | null = enrollment,
221
+ ): Promise<{
222
+ liveness: FleetLiveness;
223
+ consented: boolean;
224
+ hasDisplay: boolean;
225
+ }> {
195
226
  const { settings, bus } = services;
196
227
  let probeResponded = false;
197
- if (enrollment.status === "active") {
228
+ if (liveConnection?.connectionInstanceId) {
198
229
  const session = new SelfhostedSession({
199
230
  workspaceId,
200
231
  agentId: enrollment.id,
232
+ connectionInstanceId: liveConnection.connectionInstanceId,
233
+ // Ping does not touch the filesystem. Use the reported root when present;
234
+ // older agents can still answer liveness before they reconnect and report it.
235
+ workspaceRoot: enrollment.workspaceRoot ?? "/",
201
236
  controlRpc: controlRpc(bus),
202
237
  relay: relayConfigFromSettings(settings),
203
238
  timeoutMs: PROBE_TIMEOUT_MS,
@@ -220,29 +255,45 @@ async function probeEnrollment(
220
255
  },
221
256
  probeResponded,
222
257
  });
223
- return { liveness: state.state, consented: state.consented, hasDisplay: state.hasDisplay };
258
+ return {
259
+ liveness: state.state,
260
+ consented: state.consented,
261
+ hasDisplay: state.hasDisplay,
262
+ };
224
263
  }
225
264
 
226
265
  /**
227
266
  * List the fleet: the session's own group box when it has one (a synthetic
228
267
  * entry) + the workspace's first-class selfhosted sandboxes (each probed for
229
268
  * liveness), each with an `active` marker derived from the session's active
230
- * pointer. A backend:none session has no synthetic home entry; a null pointer
231
- * then means no compute is attached.
269
+ * pointer. A backend:none session and a machine-home session on a deployment
270
+ * without a managed provider have no synthetic group entry; a null pointer then
271
+ * means no compute is attached.
232
272
  */
233
273
  export async function listFleet(
234
274
  services: FleetServices,
235
275
  ctx: FleetContext,
236
276
  ): Promise<FleetListResult> {
237
277
  const { db } = services;
278
+ const resourceAccess = ctx.subjectId
279
+ ? {
280
+ accountId: ctx.accountId,
281
+ workspaceId: ctx.workspaceId,
282
+ subjectId: ctx.subjectId,
283
+ }
284
+ : ctx.workspaceId;
238
285
  const pointer = (await readActiveSandbox(db, ctx.workspaceId, ctx.sessionId)) ?? {
239
286
  activeSandboxId: null,
240
287
  activeEpoch: 0,
241
288
  };
242
289
 
243
290
  const entries: FleetSandboxEntry[] = [];
291
+ const groupBackend = managedSessionGroupBackend(
292
+ services.settings.sandboxBackend,
293
+ ctx.sessionBackend,
294
+ );
244
295
 
245
- if (ctx.sessionBackend !== "none") {
296
+ if (groupBackend) {
246
297
  // The session's own group box (the default/home sandbox; null active pointer ==
247
298
  // this box). A session/group row is not provider existence. Online requires a
248
299
  // warm lease, observed provider existence, and verified workspace readiness.
@@ -273,13 +324,12 @@ export async function listFleet(
273
324
  ? "unavailable"
274
325
  : groupRecovering
275
326
  ? "recovering"
276
- : ctx.sessionBackend === "selfhosted"
277
- ? "unavailable"
278
- : "wakeable";
327
+ : "wakeable";
328
+ const presentation = sessionGroupMachinePresentation(groupBackend);
279
329
  entries.push({
280
330
  id: ctx.sessionGroupId,
281
- kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
282
- name: "session sandbox",
331
+ kind: presentation.kind,
332
+ name: presentation.name,
283
333
  liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
284
334
  active: groupActive,
285
335
  isSessionGroup: true,
@@ -302,20 +352,65 @@ export async function listFleet(
302
352
 
303
353
  // The workspace's first-class selfhosted sandboxes (enrolled machines). Probe
304
354
  // each for liveness; a missing enrollment is offline.
305
- const sandboxes = await listSandboxes(db, ctx.workspaceId);
355
+ const legacySandboxes = await listSandboxes(db, ctx.workspaceId);
356
+ const enrollments = await listEnrollments(
357
+ db,
358
+ ctx.subjectId
359
+ ? {
360
+ accountId: ctx.accountId,
361
+ workspaceId: ctx.workspaceId,
362
+ subjectId: ctx.subjectId,
363
+ }
364
+ : ctx.workspaceId,
365
+ { status: "active" },
366
+ );
367
+ const enrollmentById = new Map(enrollments.map((enrollment) => [enrollment.id, enrollment]));
368
+ const scopedSandboxes: SandboxRecord[] = enrollments.flatMap((enrollment) =>
369
+ enrollment.sandboxId
370
+ ? [
371
+ {
372
+ id: enrollment.sandboxId,
373
+ accountId: enrollment.accountId,
374
+ workspaceId: enrollment.workspaceId,
375
+ kind: "selfhosted" as const,
376
+ name: enrollment.sandboxName ?? `${enrollment.os} machine`,
377
+ enrollmentId: enrollment.id,
378
+ createdAt: enrollment.createdAt,
379
+ updatedAt: enrollment.updatedAt,
380
+ },
381
+ ]
382
+ : [],
383
+ );
384
+ const sandboxes = [
385
+ ...legacySandboxes.filter(
386
+ (sandbox) => !scopedSandboxes.some((scoped) => scoped.id === sandbox.id),
387
+ ),
388
+ ...scopedSandboxes,
389
+ ];
306
390
  for (const sandbox of sandboxes) {
307
391
  if (sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
308
392
  continue;
309
393
  }
310
- const enrollment = await getEnrollment(db, ctx.workspaceId, sandbox.enrollmentId);
394
+ const enrollment =
395
+ enrollmentById.get(sandbox.enrollmentId) ??
396
+ (await getEnrollment(db, resourceAccess, sandbox.enrollmentId));
311
397
  if (!enrollment || enrollment.status !== "active") {
312
398
  // Revoked enrollments remain durable audit/history records, but they are
313
399
  // intentionally absent from the normal attach/run picker.
314
400
  continue;
315
401
  }
402
+ const liveConnection = await getLiveEnrollmentConnection(
403
+ db,
404
+ resourceAccess,
405
+ sandbox.enrollmentId,
406
+ );
316
407
  const probe = enrollment
317
- ? await probeEnrollment(services, ctx.workspaceId, enrollment)
318
- : { liveness: "offline" as FleetLiveness, consented: false, hasDisplay: false };
408
+ ? await probeEnrollment(services, sandbox.workspaceId, enrollment, liveConnection)
409
+ : {
410
+ liveness: "offline" as FleetLiveness,
411
+ consented: false,
412
+ hasDisplay: false,
413
+ };
319
414
  entries.push({
320
415
  id: sandbox.id,
321
416
  kind: "selfhosted",
@@ -360,29 +455,29 @@ export async function listFleet(
360
455
  };
361
456
  }
362
457
 
363
- /** Resolve a swap target id → the value `setActiveSandbox` writes. The session's
364
- * own group id maps to NULL (the default pointer); a first-class sandbox id is
365
- * validated (workspace ownership + liveness) and written verbatim. */
366
- async function resolveTarget(
458
+ type FleetResourceContext = Pick<FleetContext, "accountId" | "workspaceId" | "subjectId">;
459
+
460
+ type ResolvedNamedSandboxTarget =
461
+ | { ok: true; targetSandboxId: string; workspaceRoot?: string }
462
+ | { ok: false; reason: string; code: BackendUnresolvableCode };
463
+
464
+ /** Resolve one named sandbox, independent of any existing session pointer. */
465
+ async function resolveNamedSandboxTarget(
367
466
  services: FleetServices,
368
- ctx: FleetContext,
467
+ ctx: FleetResourceContext,
369
468
  target: string,
370
- ): Promise<
371
- | { ok: true; targetSandboxId: string | null }
372
- | { ok: false; reason: string; code: BackendUnresolvableCode }
373
- > {
374
- // The session's own group box → the default pointer (null).
375
- if (target === ctx.sessionGroupId || target === "session" || target === "default") {
376
- if (ctx.sessionBackend === "none") {
377
- return {
378
- ok: false,
379
- reason: "this session has no home sandbox; attach a Connected Machine",
380
- code: "unsupported_backend_context",
381
- };
382
- }
383
- return { ok: true, targetSandboxId: null };
384
- }
385
- const sandbox = await getSandbox(services.db, ctx.workspaceId, target);
469
+ ): Promise<ResolvedNamedSandboxTarget> {
470
+ const sandbox = await getSandbox(
471
+ services.db,
472
+ ctx.subjectId
473
+ ? {
474
+ accountId: ctx.accountId,
475
+ workspaceId: ctx.workspaceId,
476
+ subjectId: ctx.subjectId,
477
+ }
478
+ : ctx.workspaceId,
479
+ target,
480
+ );
386
481
  if (!sandbox) {
387
482
  return {
388
483
  ok: false,
@@ -402,8 +497,13 @@ async function resolveTarget(
402
497
  isSessionGroup: false,
403
498
  });
404
499
  if (!establishable.ok) {
405
- return { ok: false, reason: establishable.reason, code: establishable.code };
500
+ return {
501
+ ok: false,
502
+ reason: establishable.reason,
503
+ code: establishable.code,
504
+ };
406
505
  }
506
+ let targetWorkspaceRoot: string | undefined;
407
507
  if (sandbox.kind === "selfhosted") {
408
508
  if (!sandbox.enrollmentId) {
409
509
  return {
@@ -412,7 +512,17 @@ async function resolveTarget(
412
512
  code: "offline_enrollment",
413
513
  };
414
514
  }
415
- const enrollment = await getEnrollment(services.db, ctx.workspaceId, sandbox.enrollmentId);
515
+ const enrollment = await getLiveEnrollmentConnection(
516
+ services.db,
517
+ ctx.subjectId
518
+ ? {
519
+ accountId: ctx.accountId,
520
+ workspaceId: ctx.workspaceId,
521
+ subjectId: ctx.subjectId,
522
+ }
523
+ : sandbox.workspaceId,
524
+ sandbox.enrollmentId,
525
+ );
416
526
  if (!enrollment) {
417
527
  return {
418
528
  ok: false,
@@ -420,7 +530,15 @@ async function resolveTarget(
420
530
  code: "offline_enrollment",
421
531
  };
422
532
  }
423
- const probe = await probeEnrollment(services, ctx.workspaceId, enrollment);
533
+ if (!enrollment.workspaceRoot) {
534
+ return {
535
+ ok: false,
536
+ reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
537
+ code: "workspace_root_unavailable",
538
+ };
539
+ }
540
+ targetWorkspaceRoot = enrollment.workspaceRoot;
541
+ const probe = await probeEnrollment(services, sandbox.workspaceId, enrollment, enrollment);
424
542
  if (probe.liveness !== "online") {
425
543
  return {
426
544
  ok: false,
@@ -429,7 +547,86 @@ async function resolveTarget(
429
547
  };
430
548
  }
431
549
  }
432
- return { ok: true, targetSandboxId: sandbox.id };
550
+ return {
551
+ ok: true,
552
+ targetSandboxId: sandbox.id,
553
+ ...(targetWorkspaceRoot ? { workspaceRoot: targetWorkspaceRoot } : {}),
554
+ };
555
+ }
556
+
557
+ export type CreateTimeSandboxTargetPreflight =
558
+ | { ok: true; targetSandboxId: string; workingDir: string | null }
559
+ | {
560
+ ok: false;
561
+ reason: string;
562
+ code: BackendUnresolvableCode | "invalid_working_directory";
563
+ };
564
+
565
+ /**
566
+ * Validate a named create-time machine target before any session row exists.
567
+ * The caller still commits the pointer with `setActiveSandbox` inside the
568
+ * session-create transaction, which rechecks durable ownership/enrollment
569
+ * authority and closes the remove/revoke race without holding database locks
570
+ * across the liveness probe.
571
+ */
572
+ export async function preflightCreateTimeSandboxTarget(
573
+ services: FleetServices,
574
+ ctx: FleetResourceContext,
575
+ target: string,
576
+ workingDir: string | null,
577
+ ): Promise<CreateTimeSandboxTargetPreflight> {
578
+ const resolved = await resolveNamedSandboxTarget(services, ctx, target);
579
+ if (!resolved.ok) return resolved;
580
+ if (!resolved.workspaceRoot) {
581
+ return {
582
+ ok: true,
583
+ targetSandboxId: resolved.targetSandboxId,
584
+ workingDir,
585
+ };
586
+ }
587
+ try {
588
+ return {
589
+ ok: true,
590
+ targetSandboxId: resolved.targetSandboxId,
591
+ workingDir:
592
+ resolveConnectedMachineWorkspaceRoot(resolved.workspaceRoot, workingDir) ??
593
+ resolved.workspaceRoot,
594
+ };
595
+ } catch (error) {
596
+ return {
597
+ ok: false,
598
+ reason: error instanceof Error ? error.message : String(error),
599
+ code: "invalid_working_directory",
600
+ };
601
+ }
602
+ }
603
+
604
+ /** Resolve a swap target id → the value `setActiveSandbox` writes. The session's
605
+ * own group id maps to NULL (the default pointer); a first-class sandbox id is
606
+ * validated (workspace ownership + liveness) and written verbatim. */
607
+ async function resolveTarget(
608
+ services: FleetServices,
609
+ ctx: FleetContext,
610
+ target: string,
611
+ ): Promise<
612
+ | { ok: true; targetSandboxId: string | null; workspaceRoot?: string }
613
+ | { ok: false; reason: string; code: BackendUnresolvableCode }
614
+ > {
615
+ // The session's own group box → the default pointer (null).
616
+ if (target === ctx.sessionGroupId || target === "session" || target === "default") {
617
+ if (!managedSessionGroupBackend(services.settings.sandboxBackend, ctx.sessionBackend)) {
618
+ return {
619
+ ok: false,
620
+ reason:
621
+ ctx.sessionBackend === "none"
622
+ ? "this session has no home sandbox; attach a Connected Machine"
623
+ : "this deployment has no managed session sandbox; select an enrolled machine",
624
+ code: "unsupported_backend_context",
625
+ };
626
+ }
627
+ return { ok: true, targetSandboxId: null };
628
+ }
629
+ return await resolveNamedSandboxTarget(services, ctx, target);
433
630
  }
434
631
 
435
632
  /**
@@ -482,6 +679,7 @@ export async function swapActiveSandbox(
482
679
  const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
483
680
  activeSandboxId: null,
484
681
  activeEpoch: 0,
682
+ workingDir: null,
485
683
  };
486
684
  return {
487
685
  swapped: false,
@@ -503,7 +701,27 @@ export async function swapActiveSandbox(
503
701
  const pointer = (await readActiveSandbox(services.db, ctx.workspaceId, ctx.sessionId)) ?? {
504
702
  activeSandboxId: null,
505
703
  activeEpoch: 0,
704
+ workingDir: null,
506
705
  };
706
+ let effectiveWorkingDir = workingDir;
707
+ if (resolved.workspaceRoot) {
708
+ try {
709
+ effectiveWorkingDir = resolveConnectedMachineWorkspaceRoot(
710
+ resolved.workspaceRoot,
711
+ workingDir === undefined && pointer.activeSandboxId === resolved.targetSandboxId
712
+ ? pointer.workingDir
713
+ : workingDir,
714
+ );
715
+ } catch (error) {
716
+ return {
717
+ swapped: false,
718
+ activeSandboxId: pointer.activeSandboxId,
719
+ activeEpoch: pointer.activeEpoch,
720
+ reason: error instanceof Error ? error.message : String(error),
721
+ code: "invalid_working_directory",
722
+ };
723
+ }
724
+ }
507
725
  // Even a same-target attach advances active_epoch. It is a repair/fence
508
726
  // request, not a no-op acknowledgment: any cached stale route is invalidated
509
727
  // only after target readiness has been proved above.
@@ -513,7 +731,13 @@ export async function swapActiveSandbox(
513
731
  sessionId: ctx.sessionId,
514
732
  targetSandboxId: resolved.targetSandboxId,
515
733
  expectedEpoch: pointer.activeEpoch,
516
- ...(workingDir !== undefined ? { workingDir } : {}),
734
+ ...(ctx.subjectId ? { subjectId: ctx.subjectId } : {}),
735
+ ...(ctx.attemptAuthority ? { personalMachineAttempt: ctx.attemptAuthority } : {}),
736
+ ...(resolved.workspaceRoot !== undefined
737
+ ? { workingDir: effectiveWorkingDir ?? resolved.workspaceRoot }
738
+ : workingDir !== undefined
739
+ ? { workingDir }
740
+ : {}),
517
741
  });
518
742
  if (result.swapped && result.pointer) {
519
743
  return {
@@ -569,6 +793,9 @@ export type RunOnResult = {
569
793
  export type RunOnSelfhostedMachine = {
570
794
  workspaceId: string;
571
795
  agentId: string;
796
+ /** Exact live daemon pinned for this operation. */
797
+ connectionInstanceId: string;
798
+ workspaceRoot: string;
572
799
  controlRpc: ControlRpc;
573
800
  relay: SelfhostedRelayConfig;
574
801
  /** Short request/reply deadline for read/write and other control operations. */
@@ -577,8 +804,50 @@ export type RunOnSelfhostedMachine = {
577
804
  execTimeoutMs: number;
578
805
  /** Streaming transport required when execTimeoutMs is 0 (unbounded). */
579
806
  opStream?: SelfhostedOpStreamDeps;
807
+ operationResourcePolicy?: SelfhostedOperationResourcePolicy;
808
+ operationResourcePolicySupported?: boolean;
809
+ operationCpuQuotaSupported?: boolean;
810
+ /** Last-boundary authority/connection resolver invoked before every physical
811
+ * provider operation. */
812
+ resolveOperationAdmission?: () => Promise<SelfhostedOperationAdmission | null>;
813
+ /**
814
+ * Exact-attempt values for this one child process. Never persisted on the
815
+ * enrollment or machine. The caller may supply these only after authenticating
816
+ * the active attempt that owns the one-off operation.
817
+ */
818
+ transientExecEnvironment?: Readonly<Record<string, string>>;
580
819
  };
581
820
 
821
+ export type RunOnOptions = {
822
+ transientExecEnvironment?: Readonly<Record<string, string>>;
823
+ };
824
+
825
+ function runOnOperationAdmission(
826
+ services: FleetServices,
827
+ enrollment: EnrollmentRecord | null,
828
+ ): SelfhostedOperationAdmission | null {
829
+ if (!enrollment?.connectionInstanceId || !enrollment.workspaceRoot) return null;
830
+ const opStream =
831
+ services.settings.agentOpStreamEnabled === true &&
832
+ enrollment.opStream === true &&
833
+ services.bus?.getOpStreamConnection
834
+ ? {
835
+ transport: new NatsOpStreamTransport(
836
+ async () => services.bus?.getOpStreamConnection?.() ?? null,
837
+ ),
838
+ }
839
+ : undefined;
840
+ return {
841
+ workspaceId: enrollment.workspaceId,
842
+ connectionInstanceId: enrollment.connectionInstanceId,
843
+ workspaceRoot: enrollment.workspaceRoot,
844
+ ...(opStream ? { opStream } : {}),
845
+ operationResourcePolicy: enrollment.operationPolicy,
846
+ operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
847
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
848
+ };
849
+ }
850
+
582
851
  /**
583
852
  * Execute the one-off machine operation once the workspace/enrollment lookup has
584
853
  * succeeded. Kept separate from {@link runOnSandbox} so the command/deadline
@@ -593,10 +862,29 @@ export async function executeRunOnSelfhostedMachine(
593
862
  const session = new SelfhostedSession({
594
863
  workspaceId: machine.workspaceId,
595
864
  agentId: machine.agentId,
865
+ connectionInstanceId: machine.connectionInstanceId,
866
+ workspaceRoot: machine.workspaceRoot,
596
867
  controlRpc: machine.controlRpc,
597
868
  relay: machine.relay,
598
869
  timeoutMs: machine.controlTimeoutMs,
599
870
  execTimeoutMs: machine.execTimeoutMs,
871
+ ...(machine.operationResourcePolicy !== undefined
872
+ ? { operationResourcePolicy: machine.operationResourcePolicy }
873
+ : {}),
874
+ ...(machine.operationResourcePolicySupported !== undefined
875
+ ? {
876
+ operationResourcePolicySupported: machine.operationResourcePolicySupported,
877
+ }
878
+ : {}),
879
+ ...(machine.operationCpuQuotaSupported !== undefined
880
+ ? { operationCpuQuotaSupported: machine.operationCpuQuotaSupported }
881
+ : {}),
882
+ ...(machine.resolveOperationAdmission !== undefined
883
+ ? { resolveOperationAdmission: machine.resolveOperationAdmission }
884
+ : {}),
885
+ ...(machine.transientExecEnvironment !== undefined
886
+ ? { transientExecEnvironment: () => machine.transientExecEnvironment! }
887
+ : {}),
600
888
  ...(machine.opStream !== undefined ? { opStream: machine.opStream } : {}),
601
889
  });
602
890
 
@@ -622,7 +910,9 @@ export async function executeRunOnSelfhostedMachine(
622
910
  timedOut,
623
911
  deadlineMs,
624
912
  ...(timedOut
625
- ? { reason: `command exceeded the ${deadlineMs} ms execution deadline` }
913
+ ? {
914
+ reason: `command exceeded the ${deadlineMs} ms execution deadline`,
915
+ }
626
916
  : !hasTerminalExit
627
917
  ? { reason: "machine returned no terminal exit code" }
628
918
  : {}),
@@ -630,9 +920,17 @@ export async function executeRunOnSelfhostedMachine(
630
920
  }
631
921
  if (op.kind === "read") {
632
922
  const bytes = await session.readFile({ path: op.path });
633
- return { target, kind: "read", ok: true, content: new TextDecoder().decode(bytes) };
923
+ return {
924
+ target,
925
+ kind: "read",
926
+ ok: true,
927
+ content: new TextDecoder().decode(bytes),
928
+ };
634
929
  }
635
- const bytesWritten = await session.writeFile({ path: op.path, content: op.content });
930
+ const bytesWritten = await session.writeFile({
931
+ path: op.path,
932
+ content: op.content,
933
+ });
636
934
  return { target, kind: "write", ok: true, bytesWritten };
637
935
  } catch (error) {
638
936
  const reason = error instanceof Error ? error.message : String(error);
@@ -667,8 +965,19 @@ export async function runOnSandbox(
667
965
  ctx: FleetContext,
668
966
  target: string,
669
967
  op: RunOnOp,
968
+ options: RunOnOptions = {},
670
969
  ): Promise<RunOnResult> {
671
- const sandbox = await getSandbox(services.db, ctx.workspaceId, target);
970
+ const sandbox = await getSandbox(
971
+ services.db,
972
+ ctx.subjectId
973
+ ? {
974
+ accountId: ctx.accountId,
975
+ workspaceId: ctx.workspaceId,
976
+ subjectId: ctx.subjectId,
977
+ }
978
+ : ctx.workspaceId,
979
+ target,
980
+ );
672
981
  if (!sandbox) {
673
982
  return {
674
983
  target,
@@ -686,8 +995,59 @@ export async function runOnSandbox(
686
995
  reason: `run_on routes one-off ops to enrolled selfhosted machines; ${sandbox.kind} targets are reached via the active sandbox (swap to it first)`,
687
996
  };
688
997
  }
689
- const enrollment = await getEnrollment(services.db, ctx.workspaceId, sandbox.enrollmentId);
690
- if (!enrollment || enrollment.status !== "active") {
998
+ if (sandbox.scope === "user") {
999
+ if (!ctx.subjectId || !ctx.attemptAuthority) {
1000
+ return {
1001
+ target,
1002
+ targetName: sandbox.name,
1003
+ kind: op.kind,
1004
+ ok: false,
1005
+ reason: "personal Connected Machine use requires an exact admitted agent attempt",
1006
+ };
1007
+ }
1008
+ try {
1009
+ const authorized = await authorizePersonalMachineForAttempt(services.db, {
1010
+ accountId: ctx.accountId,
1011
+ workspaceId: ctx.workspaceId,
1012
+ subjectId: ctx.subjectId,
1013
+ sessionId: ctx.sessionId,
1014
+ turnId: ctx.attemptAuthority.turnId,
1015
+ attemptId: ctx.attemptAuthority.attemptId,
1016
+ executionGeneration: ctx.attemptAuthority.executionGeneration,
1017
+ enrollmentId: sandbox.enrollmentId,
1018
+ requireActiveSandbox: false,
1019
+ });
1020
+ if (!authorized) {
1021
+ return {
1022
+ target,
1023
+ targetName: sandbox.name,
1024
+ kind: op.kind,
1025
+ ok: false,
1026
+ reason: "personal Connected Machine authority was not admitted for this attempt",
1027
+ };
1028
+ }
1029
+ } catch {
1030
+ return {
1031
+ target,
1032
+ targetName: sandbox.name,
1033
+ kind: op.kind,
1034
+ ok: false,
1035
+ reason: "personal Connected Machine authority is not live for this attempt",
1036
+ };
1037
+ }
1038
+ }
1039
+ const enrollment = await getLiveEnrollmentConnection(
1040
+ services.db,
1041
+ ctx.subjectId
1042
+ ? {
1043
+ accountId: ctx.accountId,
1044
+ workspaceId: ctx.workspaceId,
1045
+ subjectId: ctx.subjectId,
1046
+ }
1047
+ : sandbox.workspaceId,
1048
+ sandbox.enrollmentId,
1049
+ );
1050
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
691
1051
  return {
692
1052
  target,
693
1053
  targetName: sandbox.name,
@@ -696,15 +1056,65 @@ export async function runOnSandbox(
696
1056
  reason: `sandbox ${target} is not enrolled/active`,
697
1057
  };
698
1058
  }
1059
+ if (!enrollment.workspaceRoot) {
1060
+ return {
1061
+ target,
1062
+ targetName: sandbox.name,
1063
+ kind: op.kind,
1064
+ ok: false,
1065
+ reason: `sandbox ${target} has not reported an absolute workspace root; reconnect it with a current agent`,
1066
+ };
1067
+ }
1068
+ const resolveOperationAdmission = async (): Promise<SelfhostedOperationAdmission | null> => {
1069
+ try {
1070
+ const current =
1071
+ sandbox.scope === "user" && ctx.subjectId && ctx.attemptAuthority
1072
+ ? await resolvePersonalMachineConnectionForAttempt(services.db, {
1073
+ accountId: ctx.accountId,
1074
+ workspaceId: ctx.workspaceId,
1075
+ subjectId: ctx.subjectId,
1076
+ sessionId: ctx.sessionId,
1077
+ turnId: ctx.attemptAuthority.turnId,
1078
+ attemptId: ctx.attemptAuthority.attemptId,
1079
+ executionGeneration: ctx.attemptAuthority.executionGeneration,
1080
+ enrollmentId: sandbox.enrollmentId!,
1081
+ requireActiveSandbox: false,
1082
+ })
1083
+ : await getLiveEnrollmentConnection(
1084
+ services.db,
1085
+ ctx.subjectId
1086
+ ? {
1087
+ accountId: ctx.accountId,
1088
+ workspaceId: ctx.workspaceId,
1089
+ subjectId: ctx.subjectId,
1090
+ }
1091
+ : sandbox.workspaceId,
1092
+ sandbox.enrollmentId!,
1093
+ );
1094
+ return runOnOperationAdmission(services, current);
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ };
699
1099
 
700
1100
  const result = await executeRunOnSelfhostedMachine(
701
1101
  {
702
- workspaceId: ctx.workspaceId,
1102
+ workspaceId: sandbox.workspaceId,
703
1103
  agentId: sandbox.enrollmentId,
1104
+ connectionInstanceId: enrollment.connectionInstanceId,
1105
+ workspaceRoot: enrollment.workspaceRoot,
704
1106
  controlRpc: controlRpc(services.bus),
705
1107
  relay: relayConfigFromSettings(services.settings),
706
1108
  controlTimeoutMs: services.settings.sandboxSelfhostedControlTimeoutMs,
707
1109
  execTimeoutMs: services.settings.sandboxSelfhostedExecTimeoutMs,
1110
+ operationResourcePolicy: enrollment.operationPolicy,
1111
+ operationResourcePolicySupported:
1112
+ enrollment.agentCapabilities.operationResourcePolicy === true,
1113
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
1114
+ resolveOperationAdmission,
1115
+ ...(options.transientExecEnvironment !== undefined
1116
+ ? { transientExecEnvironment: options.transientExecEnvironment }
1117
+ : {}),
708
1118
  ...(services.settings.agentOpStreamEnabled === true &&
709
1119
  enrollment.opStream === true &&
710
1120
  services.bus?.getOpStreamConnection
@@ -749,17 +1159,19 @@ export async function provisionSandbox(
749
1159
  input: { kind: "selfhosted" | "modal"; name?: string },
750
1160
  ): Promise<ProvisionResult> {
751
1161
  if (input.kind === "selfhosted") {
752
- const base = (services.settings.publicBaseUrl ?? "https://get.opengeni.ai").replace(/\/+$/, "");
1162
+ const base = (services.settings.publicBaseUrl ?? "https://app.opengeni.ai").replace(/\/+$/, "");
1163
+ const unixEnvironment = `OPENGENI_API_URL=${base} OPENGENI_WORKSPACE_ID=${ctx.workspaceId}`;
1164
+ const windowsEnvironment = `$env:OPENGENI_API_URL='${base}'; $env:OPENGENI_WORKSPACE_ID='${ctx.workspaceId}';`;
753
1165
  return {
754
1166
  kind: "selfhosted",
755
1167
  instructions:
756
- "Share these instructions with a human operator. They install the OpenGeni agent on the machine, run `opengeni-agent connect`, complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox. Existing connections to other OpenGeni workspaces or deployments are preserved.",
1168
+ "Share one of these deployment-specific commands with a human operator. It installs the Opengeni agent and starts `opengeni-agent connect` for this exact deployment and workspace; do not run a second bare `connect`. Complete the device-flow at the verification URL (the loud whole-machine + screen-control consent), and the machine then appears here as an attachable selfhosted sandbox. Existing connections to other Opengeni workspaces or deployments are preserved.",
757
1169
  // Install from THIS control plane's origin (not a hardcoded public CDN): the
758
1170
  // served install script is rewritten to pull the per-SHA agent baked into
759
1171
  // this exact deployment (see apps/api/src/routes/install.ts), so a deployed
760
1172
  // env is self-contained and a private/air-gapped one works with no public DNS.
761
- installCommandUnix: `curl -fsSL ${base}/install.sh | sh`,
762
- installCommandWindows: `irm ${base}/install.ps1 | iex`,
1173
+ installCommandUnix: `curl -fsSL ${base}/install.sh | ${unixEnvironment} sh`,
1174
+ installCommandWindows: `${windowsEnvironment} irm ${base}/install.ps1 | iex`,
763
1175
  verificationUri: `${base}/device`,
764
1176
  note: "Whole-machine access requires explicit human consent in the device-flow web page; the agent cannot self-consent.",
765
1177
  };