@opengeni/core 0.10.0 → 0.11.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.
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getSandbox as getSandbox2,
10
10
  listSandboxes,
11
11
  readActiveSandbox as readActiveSandbox2,
12
+ readLease,
12
13
  requireSession,
13
14
  setActiveSandbox
14
15
  } from "@opengeni/db";
@@ -21,8 +22,20 @@ import {
21
22
  import { HTTPException } from "hono/http-exception";
22
23
 
23
24
  // src/sandbox/routing.ts
24
- import { getSandbox, readActiveSandbox } from "@opengeni/db";
25
25
  import {
26
+ advanceWorkspaceGenerationForDirectRequest,
27
+ advanceWorkspaceGenerationForRetainedProcess,
28
+ getSandbox,
29
+ markWarmLeaseInstanceLost,
30
+ readActiveSandbox,
31
+ retainWorkspaceMutationProcess,
32
+ settleRetainedProcess,
33
+ verifyDirectWorkspaceMutationSettlement,
34
+ verifyRetainedProcessMutationSettlement
35
+ } from "@opengeni/db";
36
+ import { appendAndPublishEvents } from "@opengeni/events";
37
+ import {
38
+ isProviderSandboxGoneDuringRoutedOperation,
26
39
  makeActiveBackendResolver,
27
40
  NatsControlRpc,
28
41
  RoutingSandboxSession
@@ -63,6 +76,129 @@ function routingEnabled(settings) {
63
76
  }
64
77
  function wrapChannelABoxWithRouting(services, ids, established) {
65
78
  const { db, settings, bus } = services;
79
+ const beforeMutation = async ({
80
+ op,
81
+ backend
82
+ }) => {
83
+ if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0) {
84
+ return null;
85
+ }
86
+ if (backend.activeEpoch === void 0) {
87
+ throw new Error("API-direct workspace mutation resolved without an active route epoch");
88
+ }
89
+ return await advanceWorkspaceGenerationForDirectRequest(db, {
90
+ accountId: ids.accountId,
91
+ workspaceId: ids.workspaceId,
92
+ sessionId: ids.sessionId,
93
+ requestId: ids.directRequest.requestId,
94
+ holderId: ids.directRequest.holderId,
95
+ sandboxGroupId: ids.homeLease.sandboxGroupId,
96
+ expectedEpoch: backend.leaseEpoch,
97
+ expectedInstanceId: backend.providerInstanceId,
98
+ routeTargetId: backend.sandboxId,
99
+ routeEpoch: backend.activeEpoch,
100
+ operation: op
101
+ });
102
+ };
103
+ const afterMutation = async ({
104
+ op,
105
+ backend,
106
+ admission,
107
+ outcome,
108
+ retainedProcess
109
+ }) => {
110
+ if (admission === null) return;
111
+ if (!admission || typeof admission !== "object" || typeof admission.id !== "string" || typeof admission.workspaceGeneration !== "number" || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0 || backend.activeEpoch === void 0) {
112
+ throw new Error("API-direct workspace mutation settlement lacked its exact admission");
113
+ }
114
+ const exactAdmission = admission;
115
+ if (outcome === "resolved" && retainedProcess) {
116
+ await retainWorkspaceMutationProcess(db, {
117
+ accountId: ids.accountId,
118
+ workspaceId: ids.workspaceId,
119
+ sessionId: ids.sessionId,
120
+ processId: retainedProcess.id,
121
+ providerSessionId: retainedProcess.providerSessionId,
122
+ admissionId: exactAdmission.id,
123
+ admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
124
+ operation: op,
125
+ owner: {
126
+ kind: "direct",
127
+ requestId: ids.directRequest.requestId,
128
+ holderId: ids.directRequest.holderId,
129
+ sandboxGroupId: ids.homeLease.sandboxGroupId,
130
+ expectedEpoch: backend.leaseEpoch,
131
+ expectedInstanceId: backend.providerInstanceId,
132
+ routeTargetId: exactAdmission.routeTargetId,
133
+ routeEpoch: exactAdmission.routeEpoch
134
+ }
135
+ });
136
+ return;
137
+ }
138
+ await verifyDirectWorkspaceMutationSettlement(db, {
139
+ accountId: ids.accountId,
140
+ workspaceId: ids.workspaceId,
141
+ sessionId: ids.sessionId,
142
+ requestId: ids.directRequest.requestId,
143
+ holderId: ids.directRequest.holderId,
144
+ sandboxGroupId: ids.homeLease.sandboxGroupId,
145
+ expectedEpoch: backend.leaseEpoch,
146
+ expectedInstanceId: backend.providerInstanceId,
147
+ routeTargetId: exactAdmission.routeTargetId,
148
+ routeEpoch: exactAdmission.routeEpoch,
149
+ admission: exactAdmission,
150
+ operation: op,
151
+ outcome
152
+ });
153
+ };
154
+ const beforeProcessMutation = async ({
155
+ op,
156
+ process
157
+ }) => await advanceWorkspaceGenerationForRetainedProcess(db, {
158
+ accountId: ids.accountId,
159
+ workspaceId: ids.workspaceId,
160
+ sessionId: ids.sessionId,
161
+ processId: process.id,
162
+ operation: op
163
+ });
164
+ const afterProcessMutation = async ({
165
+ op,
166
+ process,
167
+ admission,
168
+ outcome
169
+ }) => {
170
+ if (!admission || typeof admission !== "object" || typeof admission.id !== "string" || typeof admission.workspaceGeneration !== "number") {
171
+ throw new Error("API retained-process mutation settlement lacked its exact admission");
172
+ }
173
+ await verifyRetainedProcessMutationSettlement(db, {
174
+ accountId: ids.accountId,
175
+ workspaceId: ids.workspaceId,
176
+ sessionId: ids.sessionId,
177
+ processId: process.id,
178
+ admission,
179
+ operation: op,
180
+ outcome
181
+ });
182
+ };
183
+ const settleProcess = async ({
184
+ backend,
185
+ process,
186
+ proof
187
+ }) => {
188
+ if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0) {
189
+ return;
190
+ }
191
+ await settleRetainedProcess(db, {
192
+ accountId: ids.accountId,
193
+ workspaceId: ids.workspaceId,
194
+ sessionId: ids.sessionId,
195
+ processId: process.id,
196
+ outcome: proof.outcome,
197
+ exitCode: proof.exitCode,
198
+ reason: proof.reason,
199
+ idleGraceMs: settings.sandboxIdleGraceMs
200
+ });
201
+ };
66
202
  const resolver = makeActiveBackendResolver({
67
203
  workspaceId: ids.workspaceId,
68
204
  defaultBackend: established.session,
@@ -77,14 +213,61 @@ function wrapChannelABoxWithRouting(services, ids, established) {
77
213
  } : null;
78
214
  },
79
215
  controlRpcFactory: controlRpcFactory(bus),
80
- relay: relayConfigFromSettings(settings)
216
+ relay: relayConfigFromSettings(settings),
217
+ resolveDefaultBackend: async () => ({
218
+ session: established.session,
219
+ sandboxId: null,
220
+ kind: established.backendId,
221
+ leaseEpoch: ids.homeLease.leaseEpoch,
222
+ providerInstanceId: ids.homeLease.instanceId
223
+ })
81
224
  });
82
225
  const proxy = new RoutingSandboxSession({
226
+ defaultResolved: {
227
+ session: established.session,
228
+ sandboxId: null,
229
+ kind: established.backendId,
230
+ leaseEpoch: ids.homeLease.leaseEpoch,
231
+ providerInstanceId: ids.homeLease.instanceId
232
+ },
83
233
  readPointer: async () => {
234
+ if (!routingEnabled(settings)) {
235
+ return { activeSandboxId: null, activeEpoch: 0 };
236
+ }
84
237
  const pointer = await readActiveSandbox(db, ids.workspaceId, ids.sessionId);
85
238
  return pointer ?? { activeSandboxId: null, activeEpoch: 0 };
86
239
  },
87
- resolveActiveBackend: resolver
240
+ resolveActiveBackend: resolver,
241
+ beforeMutation,
242
+ afterMutation,
243
+ beforeProcessMutation,
244
+ afterProcessMutation,
245
+ settleProcess,
246
+ onDefaultBackendError: async ({ error }) => {
247
+ if (!isProviderSandboxGoneDuringRoutedOperation(ids.homeLease.backend, error)) return null;
248
+ const marked = await markWarmLeaseInstanceLost(db, {
249
+ accountId: ids.accountId,
250
+ workspaceId: ids.workspaceId,
251
+ sandboxGroupId: ids.homeLease.sandboxGroupId,
252
+ expectedEpoch: ids.homeLease.leaseEpoch,
253
+ expectedInstanceId: ids.homeLease.instanceId,
254
+ diagnostic: "provider_not_found_during_routed_operation"
255
+ });
256
+ if (marked.status === "marked" && bus) {
257
+ await appendAndPublishEvents(db, bus, ids.workspaceId, ids.sessionId, [
258
+ {
259
+ type: "sandbox.box.lost",
260
+ payload: { sandboxId: ids.homeLease.instanceId }
261
+ }
262
+ ]).catch(() => void 0);
263
+ }
264
+ const lease = marked.lease;
265
+ const restore = lease?.recovery.restore.status;
266
+ return {
267
+ leaseEpoch: lease?.leaseEpoch ?? ids.homeLease.leaseEpoch,
268
+ recovery: marked.status === "stale" ? "superseded" : restore === "pending" ? "pending" : restore === "degraded" ? "degraded" : "unrecoverable"
269
+ };
270
+ }
88
271
  });
89
272
  return { ...established, session: proxy };
90
273
  }
@@ -153,15 +336,33 @@ async function listFleet(services, ctx) {
153
336
  };
154
337
  const entries = [];
155
338
  const groupActive = pointer.activeSandboxId === null;
339
+ const groupLease = await readLease(db, ctx.workspaceId, ctx.sessionGroupId);
340
+ const groupOnline = Boolean(
341
+ groupLease?.liveness === "warm" && groupLease.recovery.provider.status === "exists" && groupLease.recovery.workspace.status === "ready"
342
+ );
343
+ const groupRecovering = Boolean(
344
+ groupLease && (groupLease.liveness === "warming" || groupLease.recovery.restore.status === "pending" || groupLease.recovery.restore.status === "restoring" || groupLease.recovery.restore.status === "verifying")
345
+ );
156
346
  entries.push({
157
347
  id: ctx.sessionGroupId,
158
348
  kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
159
349
  name: "session sandbox",
160
- liveness: "online",
350
+ liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
161
351
  active: groupActive,
162
352
  isSessionGroup: true,
163
353
  enrollmentId: null,
164
- attachable: true
354
+ attachable: groupOnline,
355
+ providerStatus: groupLease?.recovery.provider.status ?? "not_created",
356
+ leaseLiveness: groupLease?.liveness ?? null,
357
+ routeStatus: groupActive ? "attached" : "detached",
358
+ archiveStatus: groupLease?.recovery.archive.status ?? "none",
359
+ restoreStatus: groupLease?.recovery.restore.status ?? "not_required",
360
+ workspaceStatus: groupLease?.recovery.workspace.status ?? "unknown",
361
+ leaseEpoch: groupLease?.leaseEpoch ?? null,
362
+ routeEpoch: pointer.activeEpoch,
363
+ workspaceGeneration: groupLease?.workspaceGeneration ?? null,
364
+ archiveGeneration: groupLease?.archiveGeneration ?? null,
365
+ archiveComplete: groupLease?.archiveComplete ?? false
165
366
  });
166
367
  const sandboxes = await listSandboxes(db, ctx.workspaceId);
167
368
  for (const sandbox of sandboxes) {
@@ -181,7 +382,18 @@ async function listFleet(services, ctx) {
181
382
  attachable: probe.liveness === "online",
182
383
  consented: probe.consented,
183
384
  hasDisplay: probe.hasDisplay,
184
- lastSeenAt: enrollment?.lastSeenAt ?? null
385
+ lastSeenAt: enrollment?.lastSeenAt ?? null,
386
+ providerStatus: probe.liveness === "online" ? "exists" : probe.liveness === "reconnecting" ? "unknown" : "missing",
387
+ leaseLiveness: null,
388
+ routeStatus: pointer.activeSandboxId === sandbox.id ? "attached" : "detached",
389
+ archiveStatus: "none",
390
+ restoreStatus: "not_required",
391
+ workspaceStatus: probe.liveness === "online" ? "ready" : "not_ready",
392
+ leaseEpoch: null,
393
+ routeEpoch: pointer.activeEpoch,
394
+ workspaceGeneration: null,
395
+ archiveGeneration: null,
396
+ archiveComplete: false
185
397
  });
186
398
  }
187
399
  return {
@@ -239,57 +451,75 @@ async function resolveTarget(services, ctx, target) {
239
451
  async function swapActiveSandbox(services, ctx, target, workingDir) {
240
452
  const resolved = await resolveTarget(services, ctx, target);
241
453
  if (!resolved.ok) {
242
- const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
454
+ const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
243
455
  activeSandboxId: null,
244
456
  activeEpoch: 0
245
457
  };
246
458
  return {
247
459
  swapped: false,
248
- activeSandboxId: pointer2.activeSandboxId,
249
- activeEpoch: pointer2.activeEpoch,
460
+ activeSandboxId: pointer.activeSandboxId,
461
+ activeEpoch: pointer.activeEpoch,
250
462
  reason: resolved.reason,
251
463
  code: resolved.code
252
464
  };
253
465
  }
254
- for (let attempt = 0; attempt < 2; attempt += 1) {
255
- const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
256
- activeSandboxId: null,
257
- activeEpoch: 0
258
- };
259
- if (pointer2.activeSandboxId === resolved.targetSandboxId) {
466
+ let readinessHold;
467
+ if (resolved.targetSandboxId === null && services.ensureSessionGroupReady) {
468
+ try {
469
+ readinessHold = await services.ensureSessionGroupReady(ctx);
470
+ } catch (error) {
471
+ const lease = await readLease(services.db, ctx.workspaceId, ctx.sessionGroupId);
472
+ const restore = lease?.recovery.restore.status;
473
+ const code = restore === "degraded" ? "recovery_degraded" : restore === "unrecoverable" ? "recovery_unrecoverable" : "recovery_in_progress";
474
+ const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
475
+ activeSandboxId: null,
476
+ activeEpoch: 0
477
+ };
260
478
  return {
261
- swapped: true,
262
- activeSandboxId: pointer2.activeSandboxId,
263
- activeEpoch: pointer2.activeEpoch
479
+ swapped: false,
480
+ activeSandboxId: pointer.activeSandboxId,
481
+ activeEpoch: pointer.activeEpoch,
482
+ reason: error instanceof Error ? error.message : "session sandbox did not reach verified readiness",
483
+ code
264
484
  };
265
485
  }
266
- const result = await setActiveSandbox(services.db, {
267
- accountId: ctx.accountId,
268
- workspaceId: ctx.workspaceId,
269
- sessionId: ctx.sessionId,
270
- targetSandboxId: resolved.targetSandboxId,
271
- expectedEpoch: pointer2.activeEpoch,
272
- ...workingDir !== void 0 ? { workingDir } : {}
273
- });
274
- if (result.swapped && result.pointer) {
275
- return {
276
- swapped: true,
277
- activeSandboxId: result.pointer.activeSandboxId,
278
- activeEpoch: result.pointer.activeEpoch
486
+ }
487
+ try {
488
+ for (let attempt = 0; attempt < 2; attempt += 1) {
489
+ const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
490
+ activeSandboxId: null,
491
+ activeEpoch: 0
279
492
  };
493
+ const result = await setActiveSandbox(services.db, {
494
+ accountId: ctx.accountId,
495
+ workspaceId: ctx.workspaceId,
496
+ sessionId: ctx.sessionId,
497
+ targetSandboxId: resolved.targetSandboxId,
498
+ expectedEpoch: pointer2.activeEpoch,
499
+ ...workingDir !== void 0 ? { workingDir } : {}
500
+ });
501
+ if (result.swapped && result.pointer) {
502
+ return {
503
+ swapped: true,
504
+ activeSandboxId: result.pointer.activeSandboxId,
505
+ activeEpoch: result.pointer.activeEpoch
506
+ };
507
+ }
280
508
  }
509
+ const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
510
+ activeSandboxId: null,
511
+ activeEpoch: 0
512
+ };
513
+ return {
514
+ swapped: false,
515
+ activeSandboxId: pointer.activeSandboxId,
516
+ activeEpoch: pointer.activeEpoch,
517
+ reason: "a concurrent swap won the epoch fence; re-read and retry",
518
+ code: "concurrent_swap"
519
+ };
520
+ } finally {
521
+ await readinessHold?.release().catch(() => void 0);
281
522
  }
282
- const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
283
- activeSandboxId: null,
284
- activeEpoch: 0
285
- };
286
- return {
287
- swapped: false,
288
- activeSandboxId: pointer.activeSandboxId,
289
- activeEpoch: pointer.activeEpoch,
290
- reason: "a concurrent swap won the epoch fence; re-read and retry",
291
- code: "concurrent_swap"
292
- };
293
523
  }
294
524
  async function runOnSandbox(services, ctx, target, op) {
295
525
  const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
@@ -3044,8 +3274,10 @@ function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDe
3044
3274
  import {
3045
3275
  createScheduledTask,
3046
3276
  deleteScheduledTask,
3277
+ getNestedAgentDepthDeploymentPolicy,
3047
3278
  getRig as getRig3,
3048
3279
  getScheduledTask,
3280
+ requireWorkspace as requireWorkspace2,
3049
3281
  updateScheduledTask
3050
3282
  } from "@opengeni/db";
3051
3283
  import { HTTPException as HTTPException10 } from "hono/http-exception";
@@ -3061,6 +3293,7 @@ import {
3061
3293
  import {
3062
3294
  CreateSessionRequest,
3063
3295
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
3296
+ SessionSpawnDenial,
3064
3297
  ServiceTurnInitiator,
3065
3298
  ServiceTurnInitiatorContext,
3066
3299
  evaluateWorkspaceModelPolicy,
@@ -3069,7 +3302,7 @@ import {
3069
3302
  } from "@opengeni/contracts";
3070
3303
  import {
3071
3304
  createSession,
3072
- createSessionWithIdempotencyKey,
3305
+ createSessionWithIdempotencyKeyResult,
3073
3306
  encryptVariableSetValue as encryptVariableSetValue2,
3074
3307
  getAnySessionInGroup,
3075
3308
  getEnrollment as getEnrollment2,
@@ -3080,7 +3313,7 @@ import {
3080
3313
  getSandbox as getSandbox3,
3081
3314
  getSession as getSession2,
3082
3315
  SessionIdConflictError,
3083
- getSessionByCreateIdempotencyKey,
3316
+ getSessionSpawnDenialByIdempotencyKey,
3084
3317
  getSessionEvent,
3085
3318
  getWorkspaceControlEvent,
3086
3319
  getSessionLineage,
@@ -3096,10 +3329,11 @@ import {
3096
3329
  withWorkspaceSubjectRls,
3097
3330
  QueueCommandConflictError,
3098
3331
  AgentCommandAuthorityError,
3332
+ SessionSpawnDeniedDbError,
3099
3333
  SessionControlConflictError
3100
3334
  } from "@opengeni/db";
3101
3335
  import {
3102
- appendAndPublishEvents,
3336
+ appendAndPublishEvents as appendAndPublishEvents2,
3103
3337
  publishDurableSessionEvents,
3104
3338
  publishDurableWorkspaceControlEvent
3105
3339
  } from "@opengeni/events";
@@ -3108,6 +3342,29 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
3108
3342
  var maxSessionMcpCredentialHeaders = 16;
3109
3343
  var maxSessionMcpCredentialHeaderValueLength = 4096;
3110
3344
  var sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
3345
+ var SessionSpawnDeniedError = class extends Error {
3346
+ denial;
3347
+ constructor(denial) {
3348
+ super(sessionSpawnDeniedMessage(denial));
3349
+ this.name = "SessionSpawnDeniedError";
3350
+ this.denial = denial;
3351
+ }
3352
+ };
3353
+ function sessionSpawnDeniedMessage(denial) {
3354
+ if (denial.code === "nested_agent_depth_override_forbidden") {
3355
+ return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
3356
+ }
3357
+ return `nested-agent depth ${denial.attemptedDepth} exceeds effective limit ${denial.effectiveMaxNestedAgentDepth} (current parent depth ${denial.currentDepth})`;
3358
+ }
3359
+ function sessionSpawnDenialEnvelope(error) {
3360
+ return {
3361
+ error: {
3362
+ code: error.denial.code,
3363
+ message: error.message,
3364
+ details: { denial: error.denial }
3365
+ }
3366
+ };
3367
+ }
3111
3368
  function serviceInitiatorForGrant(grant) {
3112
3369
  if (!grant.serviceInitiator) {
3113
3370
  if (grant.serviceInitiatorContext) {
@@ -3377,21 +3634,7 @@ async function createAndStartSession(input) {
3377
3634
  reasoningEffort: input.reasoningEffort
3378
3635
  };
3379
3636
  if (input.createIdempotencyKey) {
3380
- const existing = await getSessionByCreateIdempotencyKey(
3381
- input.db,
3382
- input.workspaceId,
3383
- input.createIdempotencyKey
3384
- );
3385
- if (existing) {
3386
- if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
3387
- throw new SessionIdConflictError(input.requestedSessionId);
3388
- }
3389
- return await finishStartSession(
3390
- existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
3391
- existing
3392
- );
3393
- }
3394
- const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
3637
+ const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
3395
3638
  ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
3396
3639
  accountId: input.accountId,
3397
3640
  workspaceId: input.workspaceId,
@@ -3415,8 +3658,15 @@ async function createAndStartSession(input) {
3415
3658
  createIdempotencyKey: input.createIdempotencyKey,
3416
3659
  sandboxGroupId: input.sandboxGroupId ?? null,
3417
3660
  ...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
3418
- mcpServers: input.mcpServers ?? []
3661
+ mcpServers: input.mcpServers ?? [],
3662
+ maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
3663
+ allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
3664
+ subjectId: input.subjectId ?? null
3419
3665
  });
3666
+ if (keyedResult.denied) {
3667
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
3668
+ }
3669
+ const { session: keyed, created } = keyedResult;
3420
3670
  if (!created) {
3421
3671
  return await finishStartSession(
3422
3672
  keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
@@ -3425,31 +3675,42 @@ async function createAndStartSession(input) {
3425
3675
  }
3426
3676
  return await finishStartSession(input, keyed);
3427
3677
  }
3428
- const session = await createSession(input.db, {
3429
- ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
3430
- accountId: input.accountId,
3431
- workspaceId: input.workspaceId,
3432
- initialMessage: input.initialMessage,
3433
- initialTurnInstructions: input.turnInstructions ?? null,
3434
- resources: input.resources,
3435
- tools: input.tools,
3436
- ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
3437
- metadata: sessionMetadata,
3438
- ...input.createdBy ? { createdBy: input.createdBy } : {},
3439
- ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3440
- createdByActor: input.createdByActor ?? null,
3441
- model: input.model,
3442
- sandboxBackend: input.sandboxBackend,
3443
- variableSetId: input.variableSet?.id ?? null,
3444
- rigId: input.rigId ?? null,
3445
- rigVersionId: input.rigVersionId ?? null,
3446
- firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
3447
- instructions: input.instructions ?? null,
3448
- parentSessionId: input.parentSessionId ?? null,
3449
- sandboxGroupId: input.sandboxGroupId ?? null,
3450
- ...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
3451
- mcpServers: input.mcpServers ?? []
3452
- });
3678
+ let session;
3679
+ try {
3680
+ session = await createSession(input.db, {
3681
+ ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
3682
+ accountId: input.accountId,
3683
+ workspaceId: input.workspaceId,
3684
+ initialMessage: input.initialMessage,
3685
+ initialTurnInstructions: input.turnInstructions ?? null,
3686
+ resources: input.resources,
3687
+ tools: input.tools,
3688
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
3689
+ metadata: sessionMetadata,
3690
+ ...input.createdBy ? { createdBy: input.createdBy } : {},
3691
+ ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3692
+ createdByActor: input.createdByActor ?? null,
3693
+ model: input.model,
3694
+ sandboxBackend: input.sandboxBackend,
3695
+ variableSetId: input.variableSet?.id ?? null,
3696
+ rigId: input.rigId ?? null,
3697
+ rigVersionId: input.rigVersionId ?? null,
3698
+ firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
3699
+ instructions: input.instructions ?? null,
3700
+ parentSessionId: input.parentSessionId ?? null,
3701
+ sandboxGroupId: input.sandboxGroupId ?? null,
3702
+ ...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
3703
+ mcpServers: input.mcpServers ?? [],
3704
+ maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
3705
+ allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
3706
+ subjectId: input.subjectId ?? null
3707
+ });
3708
+ } catch (error) {
3709
+ if (error instanceof SessionSpawnDeniedDbError) {
3710
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
3711
+ }
3712
+ throw error;
3713
+ }
3453
3714
  return await finishStartSession(input, session);
3454
3715
  }
3455
3716
  async function finishStartSession(input, session) {
@@ -3496,7 +3757,8 @@ async function finishStartSession(input, session) {
3496
3757
  text: input.goal.text,
3497
3758
  ...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
3498
3759
  ...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
3499
- } : null
3760
+ } : null,
3761
+ consumeNewSessionDraft: input.consumeNewSessionDraft ?? null
3500
3762
  });
3501
3763
  await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
3502
3764
  if (started.workflowWakeRevision !== null) {
@@ -3672,6 +3934,16 @@ async function postUserMessageTurn(input) {
3672
3934
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3673
3935
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3674
3936
  const payload = CreateSessionRequest.parse(rawPayload);
3937
+ if (payload.idempotencyKey) {
3938
+ const denial = await getSessionSpawnDenialByIdempotencyKey(
3939
+ db,
3940
+ workspaceId,
3941
+ payload.idempotencyKey
3942
+ );
3943
+ if (denial) {
3944
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
3945
+ }
3946
+ }
3675
3947
  const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3676
3948
  if (parentSessionId) {
3677
3949
  await requireSessionAuthorization(deps, grant, {
@@ -3962,11 +4234,18 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3962
4234
  sessionMcpServers: sessionMcpServers.metadata,
3963
4235
  parentSessionId,
3964
4236
  createIdempotencyKey: payload.idempotencyKey ?? null,
4237
+ maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
4238
+ allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
4239
+ subjectId: grant.subjectId,
3965
4240
  // Create-time machine targeting (A-2a): when a target sandbox is named, the
3966
4241
  // active-sandbox pointer is seeded race-free inside createAndStartSession
3967
4242
  // (after the row exists, before the first turn dispatches). Validation
3968
4243
  // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
3969
- seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null
4244
+ seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null,
4245
+ consumeNewSessionDraft: payload.expectedNewSessionDraftRevision !== void 0 ? {
4246
+ subjectId: grant.subjectId,
4247
+ expectedRevision: payload.expectedNewSessionDraftRevision
4248
+ } : null
3970
4249
  });
3971
4250
  } catch (error) {
3972
4251
  if (error instanceof AgentCommandAuthorityError) {
@@ -4136,7 +4415,7 @@ async function updateSessionTitle(deps, grant, sessionId, title, source) {
4136
4415
  const workspaceId = grant.workspaceId;
4137
4416
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
4138
4417
  if (result.updated) {
4139
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
4418
+ await appendAndPublishEvents2(db, bus, workspaceId, sessionId, [
4140
4419
  {
4141
4420
  type: "session.title_set",
4142
4421
  payload: {
@@ -4333,6 +4612,7 @@ async function validatedScheduledTaskUpdate(input) {
4333
4612
  settings: input.settings,
4334
4613
  db: input.db,
4335
4614
  objectStorage: input.objectStorage,
4615
+ grant: input.grant,
4336
4616
  workspaceId: input.existing.workspaceId,
4337
4617
  payload: { agentConfig: input.payload.agentConfig },
4338
4618
  ...input.toolsProvided !== void 0 ? { toolsProvided: input.toolsProvided } : {}
@@ -4415,6 +4695,18 @@ async function validateScheduledTaskAgentConfig(input) {
4415
4695
  throw new HTTPException10(503, { message: "object storage is not configured" });
4416
4696
  }
4417
4697
  await validateFileResources(input.db, input.workspaceId, resources);
4698
+ const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
4699
+ if (requestedMaxDepth !== void 0) {
4700
+ const workspace = await requireWorkspace2(input.db, input.workspaceId);
4701
+ const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
4702
+ const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
4703
+ const inheritedMaxDepth = typeof workspaceMaxDepth === "number" ? workspaceMaxDepth : deploymentPolicy.maxNestedAgentDepth;
4704
+ if (requestedMaxDepth > inheritedMaxDepth && !hasPermission(input.grant.permissions, "workspace:admin")) {
4705
+ throw new HTTPException10(403, {
4706
+ message: `scheduled task maxNestedAgentDepth ${requestedMaxDepth} exceeds inherited limit ${inheritedMaxDepth}; workspace:admin is required to increase it`
4707
+ });
4708
+ }
4709
+ }
4418
4710
  return {
4419
4711
  ...input.payload.agentConfig,
4420
4712
  ...model === void 0 || model === null ? {} : { model },
@@ -4485,6 +4777,102 @@ function assertWorkspaceDeletable(input) {
4485
4777
  }
4486
4778
  }
4487
4779
 
4780
+ // src/application/new-session-drafts.ts
4781
+ import {
4782
+ NewSessionDraft,
4783
+ SaveNewSessionDraftRequest
4784
+ } from "@opengeni/contracts";
4785
+ import {
4786
+ getNewSessionDraftInTransaction,
4787
+ NewSessionDraftAccessError,
4788
+ saveNewSessionDraftInTransaction,
4789
+ withWorkspaceSubjectRls as withWorkspaceSubjectRls2
4790
+ } from "@opengeni/db";
4791
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
4792
+ function mapNewSessionDraft(row) {
4793
+ if (!row) return null;
4794
+ return NewSessionDraft.parse({
4795
+ revision: row.revision,
4796
+ text: row.text,
4797
+ resources: row.resources,
4798
+ tools: row.tools,
4799
+ model: row.model,
4800
+ reasoningEffort: row.reasoningEffort,
4801
+ options: row.sessionOptions,
4802
+ updatedAt: row.updatedAt.toISOString()
4803
+ });
4804
+ }
4805
+ async function getActorNewSessionDraft(deps, grant, workspaceId) {
4806
+ const row = await withWorkspaceSubjectRls2(
4807
+ deps.db,
4808
+ workspaceId,
4809
+ grant.subjectId,
4810
+ (scoped) => getNewSessionDraftInTransaction(scoped, {
4811
+ workspaceId,
4812
+ subjectId: grant.subjectId
4813
+ })
4814
+ );
4815
+ return mapNewSessionDraft(row) ?? {
4816
+ revision: 0,
4817
+ text: "",
4818
+ resources: [],
4819
+ tools: [],
4820
+ model: deps.settings.openaiModel,
4821
+ reasoningEffort: deps.settings.openaiReasoningEffort,
4822
+ options: {},
4823
+ updatedAt: null
4824
+ };
4825
+ }
4826
+ async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
4827
+ const input = SaveNewSessionDraftRequest.parse(rawInput);
4828
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
4829
+ deps.db,
4830
+ workspaceId,
4831
+ deps.settings
4832
+ );
4833
+ const resources = normalizeResources(input.resources);
4834
+ const tools = validateToolRefs(input.tools, runtimeSettings);
4835
+ await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
4836
+ if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
4837
+ throw new HTTPException12(503, { message: "object storage is not configured" });
4838
+ }
4839
+ await validateFileResources(deps.db, workspaceId, resources);
4840
+ assertConfiguredModel(deps.settings, input.model);
4841
+ await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
4842
+ try {
4843
+ const saved = await withWorkspaceSubjectRls2(
4844
+ deps.db,
4845
+ workspaceId,
4846
+ grant.subjectId,
4847
+ (scoped) => scoped.transaction(
4848
+ (tx) => saveNewSessionDraftInTransaction(tx, {
4849
+ accountId: grant.accountId,
4850
+ workspaceId,
4851
+ subjectId: grant.subjectId,
4852
+ expectedRevision: input.expectedRevision,
4853
+ text: input.text,
4854
+ resources,
4855
+ tools,
4856
+ model: input.model,
4857
+ reasoningEffort: input.reasoningEffort,
4858
+ options: input.options,
4859
+ // Only managed people are removed through removeWorkspaceMember().
4860
+ // API keys and delegated service actors (for example the first-party
4861
+ // worker MCP principal) legitimately have no workspace_memberships
4862
+ // row, so they must not be rejected by the human-removal fence.
4863
+ requireWorkspaceMembership: grant.subjectId.startsWith("user:")
4864
+ })
4865
+ )
4866
+ );
4867
+ return mapNewSessionDraft(saved);
4868
+ } catch (error) {
4869
+ if (error instanceof NewSessionDraftAccessError) {
4870
+ throw new HTTPException12(403, { message: error.message });
4871
+ }
4872
+ throw error;
4873
+ }
4874
+ }
4875
+
4488
4876
  // src/application/session-commands.ts
4489
4877
  import { reasoningEffortForMetadata as reasoningEffortForMetadata2 } from "@opengeni/contracts";
4490
4878
  import {
@@ -4506,7 +4894,7 @@ import {
4506
4894
  steerAgentSessionInTransaction,
4507
4895
  steerQueuedTurnInTransaction,
4508
4896
  withWorkspaceRls,
4509
- withWorkspaceSubjectRls as withWorkspaceSubjectRls2
4897
+ withWorkspaceSubjectRls as withWorkspaceSubjectRls3
4510
4898
  } from "@opengeni/db";
4511
4899
  import {
4512
4900
  publishDurableSessionEvents as publishDurableSessionEvents2,
@@ -4798,7 +5186,7 @@ async function deleteHumanQueuePrompt(deps, context, turnId, input) {
4798
5186
  }
4799
5187
  async function editHumanQueuePrompt(deps, context, turnId, input) {
4800
5188
  const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
4801
- const result = await withWorkspaceSubjectRls2(
5189
+ const result = await withWorkspaceSubjectRls3(
4802
5190
  deps.db,
4803
5191
  context.workspaceId,
4804
5192
  context.subjectId,
@@ -4920,7 +5308,7 @@ async function controlHumanWorkspace(deps, context, input) {
4920
5308
  }
4921
5309
  async function getHumanComposerDraft(deps, context) {
4922
5310
  await authorizeHumanSessionCommand(deps, context, "session.composer.read");
4923
- const row = await withWorkspaceSubjectRls2(
5311
+ const row = await withWorkspaceSubjectRls3(
4924
5312
  deps.db,
4925
5313
  context.workspaceId,
4926
5314
  context.subjectId,
@@ -4949,7 +5337,7 @@ async function getHumanComposerDraft(deps, context) {
4949
5337
  }
4950
5338
  async function saveHumanComposerDraft(deps, context, input) {
4951
5339
  await authorizeHumanSessionCommand(deps, context, "session.composer.write");
4952
- const row = await withWorkspaceSubjectRls2(
5340
+ const row = await withWorkspaceSubjectRls3(
4953
5341
  deps.db,
4954
5342
  context.workspaceId,
4955
5343
  context.subjectId,
@@ -4977,6 +5365,7 @@ export {
4977
5365
  SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
4978
5366
  SessionAuthorizationDeniedError,
4979
5367
  SessionAuthorizationUnavailableError,
5368
+ SessionSpawnDeniedError,
4980
5369
  acceptSessionUserMessage,
4981
5370
  activateRigVersionForApi,
4982
5371
  appendRigSetupCommand,
@@ -5012,6 +5401,7 @@ export {
5012
5401
  editHumanQueuePrompt,
5013
5402
  enableCapability,
5014
5403
  enabledCapabilityMcpToolRefs,
5404
+ getActorNewSessionDraft,
5015
5405
  getCapabilityPack,
5016
5406
  getHumanComposerDraft,
5017
5407
  hasPermission,
@@ -5062,11 +5452,13 @@ export {
5062
5452
  rigActorForGrant,
5063
5453
  routingEnabled,
5064
5454
  runOnSandbox,
5455
+ saveActorNewSessionDraft,
5065
5456
  saveHumanComposerDraft,
5066
5457
  scheduledTaskTemporalScheduleId,
5067
5458
  scheduledTaskToolsProvided,
5068
5459
  scheduledTaskTriggerToken,
5069
5460
  sendAgentSessionMessage,
5461
+ sessionSpawnDenialEnvelope,
5070
5462
  sessionWithEffectiveToolPolicy,
5071
5463
  settingsWithEnabledCapabilityMcpServers,
5072
5464
  settingsWithMcpCapabilityServers,