@opengeni/core 0.8.0 → 0.11.1

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);
@@ -2698,6 +2928,22 @@ function enabledCapabilityMcpToolRefs(settings, runtimeSettings) {
2698
2928
  function withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings) {
2699
2929
  return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
2700
2930
  }
2931
+ function availableToolRefs(tools, settings) {
2932
+ const available = new Set(settings.mcpServers.map((server) => server.id));
2933
+ return tools.filter((tool) => available.has(tool.id));
2934
+ }
2935
+ function assertToolRefsSubset(requested, allowed, message = "requested tools exceed the session tool policy") {
2936
+ const allowedIds = new Set(allowed.map((tool) => `${tool.kind}:${tool.id}`));
2937
+ const widened = requested.find((tool) => !allowedIds.has(`${tool.kind}:${tool.id}`));
2938
+ if (widened) {
2939
+ throw new HTTPException8(403, { message: `${message}: ${widened.id}` });
2940
+ }
2941
+ }
2942
+ function validateToolRefsForSessionPolicy(input) {
2943
+ const validated = validateToolRefs(input.requested, input.settings);
2944
+ assertToolRefsSubset(validated, input.allowedTools, input.message);
2945
+ return validated;
2946
+ }
2701
2947
  function normalizeResources(resources) {
2702
2948
  const mountPaths = /* @__PURE__ */ new Map();
2703
2949
  const identities = /* @__PURE__ */ new Map();
@@ -2902,29 +3148,161 @@ function positiveInteger(value) {
2902
3148
  return null;
2903
3149
  }
2904
3150
 
3151
+ // src/domain/session-tool-policy.ts
3152
+ import {
3153
+ SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT,
3154
+ SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH,
3155
+ mergeToolRefs as mergeToolRefs2
3156
+ } from "@opengeni/contracts";
3157
+ var MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"];
3158
+ var PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
3159
+ function sortedIds(ids) {
3160
+ return [...new Set(ids)].sort();
3161
+ }
3162
+ function projectIds(ids) {
3163
+ const projectable = ids.filter(
3164
+ (id) => id.length <= SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH && PROJECTABLE_REGISTRY_ID.test(id)
3165
+ );
3166
+ return {
3167
+ ids: projectable.slice(0, SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT),
3168
+ truncated: projectable.length !== ids.length || projectable.length > SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT
3169
+ };
3170
+ }
3171
+ function resolveSessionToolPolicy(input) {
3172
+ const policy = input.toolPolicy ?? { mode: "legacy", inheritedFromSessionId: null };
3173
+ const availableIds = new Set(input.availableMcpServerIds);
3174
+ const defaultIds = new Set(input.defaultMcpServerIds ?? []);
3175
+ const mandatoryIds = MANDATORY_SESSION_MCP_SERVER_IDS.filter(
3176
+ (id) => availableIds.has(id)
3177
+ );
3178
+ const mandatoryIdSet = new Set(mandatoryIds);
3179
+ const selectedRefs = input.turnToolsProvided === true ? mergeToolRefs2([], input.turnTools ?? []) : input.turnToolsProvided === false ? mergeToolRefs2([], input.sessionTools) : mergeToolRefs2(input.sessionTools, input.turnTools ?? []);
3180
+ const tracksWorkspaceDefaults = policy.mode === "workspace_default" && input.turnToolsProvided !== true;
3181
+ let toolRefs = selectedRefs.filter((tool) => tool.optional !== true || availableIds.has(tool.id));
3182
+ if (tracksWorkspaceDefaults) {
3183
+ toolRefs = mergeToolRefs2(
3184
+ toolRefs,
3185
+ sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true }))
3186
+ );
3187
+ }
3188
+ toolRefs = mergeToolRefs2(
3189
+ toolRefs,
3190
+ mandatoryIds.map((id) => ({ kind: "mcp", id }))
3191
+ );
3192
+ const requestedEffectiveRefs = mergeToolRefs2(
3193
+ selectedRefs,
3194
+ tracksWorkspaceDefaults ? sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true })) : []
3195
+ );
3196
+ const effectiveIds = sortedIds(
3197
+ mergeToolRefs2(
3198
+ requestedEffectiveRefs,
3199
+ mandatoryIds.map((id) => ({ kind: "mcp", id }))
3200
+ ).map((tool) => tool.id)
3201
+ );
3202
+ const configuredIds = effectiveIds.filter((id) => availableIds.has(id));
3203
+ const configuredIdSet = new Set(configuredIds);
3204
+ const droppedIds = effectiveIds.filter((id) => !configuredIdSet.has(id));
3205
+ const deferredIds = tracksWorkspaceDefaults ? sortedIds(
3206
+ toolRefs.filter(
3207
+ (tool) => tool.optional === true && configuredIdSet.has(tool.id) && !mandatoryIdSet.has(tool.id)
3208
+ ).map((tool) => tool.id)
3209
+ ) : [];
3210
+ const selectedIds = sortedIds(
3211
+ selectedRefs.filter(
3212
+ (tool) => !mandatoryIdSet.has(tool.id) && !(tracksWorkspaceDefaults && tool.optional === true)
3213
+ ).map((tool) => tool.id)
3214
+ );
3215
+ const projections = {
3216
+ selected: projectIds(selectedIds),
3217
+ effective: projectIds(effectiveIds),
3218
+ mandatory: projectIds(sortedIds(mandatoryIds)),
3219
+ deferred: projectIds(deferredIds),
3220
+ configured: projectIds(configuredIds),
3221
+ dropped: projectIds(droppedIds)
3222
+ };
3223
+ return {
3224
+ toolRefs,
3225
+ effectivePolicy: {
3226
+ mode: policy.mode,
3227
+ inheritedFromSessionId: policy.inheritedFromSessionId,
3228
+ selectedIds: projections.selected.ids,
3229
+ effectiveIds: projections.effective.ids,
3230
+ mandatoryIds: projections.mandatory.ids,
3231
+ lazyRouter: {
3232
+ state: tracksWorkspaceDefaults ? "required" : "disabled",
3233
+ deferredIds: projections.deferred.ids
3234
+ },
3235
+ configuredIds: projections.configured.ids,
3236
+ droppedIds: projections.dropped.ids,
3237
+ counts: {
3238
+ selected: selectedIds.length,
3239
+ effective: effectiveIds.length,
3240
+ mandatory: mandatoryIds.length,
3241
+ deferred: deferredIds.length,
3242
+ configured: configuredIds.length,
3243
+ dropped: droppedIds.length
3244
+ },
3245
+ idsTruncated: Object.values(projections).some((projection) => projection.truncated)
3246
+ }
3247
+ };
3248
+ }
3249
+ async function workspaceSessionToolPolicyServerIds(db, workspaceId, settings) {
3250
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3251
+ return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
3252
+ }
3253
+ async function workspaceSessionToolPolicyDefaultServerIds(db, workspaceId, settings) {
3254
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3255
+ return sortedIds(enabledCapabilityMcpToolRefs(settings, runtimeSettings).map((tool) => tool.id));
3256
+ }
3257
+ function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds = []) {
3258
+ const availableIds = new Set(workspaceServerIds);
3259
+ for (const server of session.mcpServers) {
3260
+ availableIds.add(server.id);
3261
+ }
3262
+ return {
3263
+ ...session,
3264
+ effectiveToolPolicy: resolveSessionToolPolicy({
3265
+ ...session.toolPolicy ? { toolPolicy: session.toolPolicy } : {},
3266
+ sessionTools: session.tools,
3267
+ availableMcpServerIds: availableIds,
3268
+ defaultMcpServerIds: workspaceDefaultServerIds
3269
+ }).effectivePolicy
3270
+ };
3271
+ }
3272
+
2905
3273
  // src/domain/scheduled-tasks.ts
2906
3274
  import {
2907
3275
  createScheduledTask,
2908
3276
  deleteScheduledTask,
3277
+ getNestedAgentDepthDeploymentPolicy,
2909
3278
  getRig as getRig3,
2910
3279
  getScheduledTask,
3280
+ requireWorkspace as requireWorkspace2,
2911
3281
  updateScheduledTask
2912
3282
  } from "@opengeni/db";
2913
3283
  import { HTTPException as HTTPException10 } from "hono/http-exception";
2914
3284
 
2915
3285
  // src/domain/sessions.ts
2916
3286
  import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
2917
- import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
3287
+ import {
3288
+ canonicalizeConfiguredModelId,
3289
+ configuredAllowedModels,
3290
+ policyProviderIdForModel,
3291
+ resolveTurnExecutionPolicyV1
3292
+ } from "@opengeni/config";
2918
3293
  import {
2919
3294
  CreateSessionRequest,
3295
+ DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
3296
+ SessionSpawnDenial,
2920
3297
  ServiceTurnInitiator,
2921
3298
  ServiceTurnInitiatorContext,
2922
3299
  evaluateWorkspaceModelPolicy,
2923
- reasoningEffortForMetadata
3300
+ reasoningEffortForMetadata,
3301
+ SessionMcpApprovalPolicy
2924
3302
  } from "@opengeni/contracts";
2925
3303
  import {
2926
3304
  createSession,
2927
- createSessionWithIdempotencyKey,
3305
+ createSessionWithIdempotencyKeyResult,
2928
3306
  encryptVariableSetValue as encryptVariableSetValue2,
2929
3307
  getAnySessionInGroup,
2930
3308
  getEnrollment as getEnrollment2,
@@ -2935,7 +3313,7 @@ import {
2935
3313
  getSandbox as getSandbox3,
2936
3314
  getSession as getSession2,
2937
3315
  SessionIdConflictError,
2938
- getSessionByCreateIdempotencyKey,
3316
+ getSessionSpawnDenialByIdempotencyKey,
2939
3317
  getSessionEvent,
2940
3318
  getWorkspaceControlEvent,
2941
3319
  getSessionLineage,
@@ -2946,14 +3324,16 @@ import {
2946
3324
  listSessionMcpServersForChildInheritance,
2947
3325
  requireSession as requireSession2,
2948
3326
  submitHumanPromptInTransaction,
3327
+ appendSessionEventsWithLockedSessionUpdate,
2949
3328
  updateSessionTitle as updateSessionTitleRow,
2950
3329
  withWorkspaceSubjectRls,
2951
3330
  QueueCommandConflictError,
2952
3331
  AgentCommandAuthorityError,
3332
+ SessionSpawnDeniedDbError,
2953
3333
  SessionControlConflictError
2954
3334
  } from "@opengeni/db";
2955
3335
  import {
2956
- appendAndPublishEvents,
3336
+ appendAndPublishEvents as appendAndPublishEvents2,
2957
3337
  publishDurableSessionEvents,
2958
3338
  publishDurableWorkspaceControlEvent
2959
3339
  } from "@opengeni/events";
@@ -2962,6 +3342,29 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
2962
3342
  var maxSessionMcpCredentialHeaders = 16;
2963
3343
  var maxSessionMcpCredentialHeaderValueLength = 4096;
2964
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
+ }
2965
3368
  function serviceInitiatorForGrant(grant) {
2966
3369
  if (!grant.serviceInitiator) {
2967
3370
  if (grant.serviceInitiatorContext) {
@@ -3091,6 +3494,7 @@ function mcpServerConfigFromMetadata(server) {
3091
3494
  ...server.name ? { name: server.name } : {},
3092
3495
  url: server.url,
3093
3496
  cacheToolsList: false,
3497
+ requireApproval: server.requireApproval,
3094
3498
  ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
3095
3499
  };
3096
3500
  }
@@ -3151,6 +3555,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
3151
3555
  url: server.url,
3152
3556
  headerNames: Object.keys(headersEncrypted).sort(),
3153
3557
  credentialVersion: 1,
3558
+ requireApproval: server.requireApproval ?? false,
3154
3559
  connectionRef: server.connectionRef ?? null
3155
3560
  });
3156
3561
  }
@@ -3186,6 +3591,7 @@ function validateInheritedSessionMcpServersForCreate(servers) {
3186
3591
  url: server.url,
3187
3592
  headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
3188
3593
  credentialVersion: 1,
3594
+ requireApproval: server.requireApproval ?? false,
3189
3595
  connectionRef: server.connectionRef ?? null
3190
3596
  }))
3191
3597
  };
@@ -3228,21 +3634,7 @@ async function createAndStartSession(input) {
3228
3634
  reasoningEffort: input.reasoningEffort
3229
3635
  };
3230
3636
  if (input.createIdempotencyKey) {
3231
- const existing = await getSessionByCreateIdempotencyKey(
3232
- input.db,
3233
- input.workspaceId,
3234
- input.createIdempotencyKey
3235
- );
3236
- if (existing) {
3237
- if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
3238
- throw new SessionIdConflictError(input.requestedSessionId);
3239
- }
3240
- return await finishStartSession(
3241
- existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
3242
- existing
3243
- );
3244
- }
3245
- const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
3637
+ const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
3246
3638
  ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
3247
3639
  accountId: input.accountId,
3248
3640
  workspaceId: input.workspaceId,
@@ -3250,6 +3642,7 @@ async function createAndStartSession(input) {
3250
3642
  initialTurnInstructions: input.turnInstructions ?? null,
3251
3643
  resources: input.resources,
3252
3644
  tools: input.tools,
3645
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
3253
3646
  metadata: sessionMetadata,
3254
3647
  ...input.createdBy ? { createdBy: input.createdBy } : {},
3255
3648
  ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
@@ -3265,8 +3658,15 @@ async function createAndStartSession(input) {
3265
3658
  createIdempotencyKey: input.createIdempotencyKey,
3266
3659
  sandboxGroupId: input.sandboxGroupId ?? null,
3267
3660
  ...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
3268
- mcpServers: input.mcpServers ?? []
3661
+ mcpServers: input.mcpServers ?? [],
3662
+ maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
3663
+ allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
3664
+ subjectId: input.subjectId ?? null
3269
3665
  });
3666
+ if (keyedResult.denied) {
3667
+ throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
3668
+ }
3669
+ const { session: keyed, created } = keyedResult;
3270
3670
  if (!created) {
3271
3671
  return await finishStartSession(
3272
3672
  keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
@@ -3275,30 +3675,42 @@ async function createAndStartSession(input) {
3275
3675
  }
3276
3676
  return await finishStartSession(input, keyed);
3277
3677
  }
3278
- const session = await createSession(input.db, {
3279
- ...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
3280
- accountId: input.accountId,
3281
- workspaceId: input.workspaceId,
3282
- initialMessage: input.initialMessage,
3283
- initialTurnInstructions: input.turnInstructions ?? null,
3284
- resources: input.resources,
3285
- tools: input.tools,
3286
- metadata: sessionMetadata,
3287
- ...input.createdBy ? { createdBy: input.createdBy } : {},
3288
- ...input.createdByContext ? { createdByContext: input.createdByContext } : {},
3289
- createdByActor: input.createdByActor ?? null,
3290
- model: input.model,
3291
- sandboxBackend: input.sandboxBackend,
3292
- variableSetId: input.variableSet?.id ?? null,
3293
- rigId: input.rigId ?? null,
3294
- rigVersionId: input.rigVersionId ?? null,
3295
- firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
3296
- instructions: input.instructions ?? null,
3297
- parentSessionId: input.parentSessionId ?? null,
3298
- sandboxGroupId: input.sandboxGroupId ?? null,
3299
- ...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
3300
- mcpServers: input.mcpServers ?? []
3301
- });
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
+ }
3302
3714
  return await finishStartSession(input, session);
3303
3715
  }
3304
3716
  async function finishStartSession(input, session) {
@@ -3335,7 +3747,9 @@ async function finishStartSession(input, session) {
3335
3747
  sessionId: session.id,
3336
3748
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {},
3337
3749
  reasoningEffortFallback: input.reasoningEffort,
3750
+ turnExecutionPolicy: input.turnExecutionPolicy,
3338
3751
  createdEventPayload: {
3752
+ ...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
3339
3753
  ...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
3340
3754
  ...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
3341
3755
  },
@@ -3343,7 +3757,8 @@ async function finishStartSession(input, session) {
3343
3757
  text: input.goal.text,
3344
3758
  ...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
3345
3759
  ...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
3346
- } : null
3760
+ } : null,
3761
+ consumeNewSessionDraft: input.consumeNewSessionDraft ?? null
3347
3762
  });
3348
3763
  await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
3349
3764
  if (started.workflowWakeRevision !== null) {
@@ -3362,31 +3777,42 @@ async function finishStartSession(input, session) {
3362
3777
  function workflowIdForSession(sessionId) {
3363
3778
  return `session-${sessionId}`;
3364
3779
  }
3365
- function assertConfiguredModel(settings, model) {
3780
+ function canonicalConfiguredModel(settings, model) {
3366
3781
  if (model === null || model === void 0) {
3367
- return;
3782
+ return model;
3368
3783
  }
3369
- if (configuredAllowedModels(settings).includes(model)) {
3370
- return;
3784
+ const canonicalModel = canonicalizeConfiguredModelId(settings, model);
3785
+ if (configuredAllowedModels(settings).includes(canonicalModel)) {
3786
+ return canonicalModel;
3371
3787
  }
3372
- if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
3373
- return;
3788
+ if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
3789
+ return canonicalModel;
3374
3790
  }
3375
3791
  throw new HTTPException9(422, { message: `model is not available: ${model}` });
3376
3792
  }
3793
+ function assertConfiguredModel(settings, model) {
3794
+ canonicalConfiguredModel(settings, model);
3795
+ }
3377
3796
  async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
3378
3797
  if (model === null || model === void 0) {
3379
3798
  return;
3380
3799
  }
3800
+ const canonicalModel = canonicalConfiguredModel(settings, model);
3801
+ if (canonicalModel === null || canonicalModel === void 0) {
3802
+ return;
3803
+ }
3381
3804
  const policy = await getWorkspaceModelPolicy(db, workspaceId);
3382
3805
  if (!policy) {
3383
3806
  return;
3384
3807
  }
3385
- const providerId = policyProviderIdForModel(settings, model);
3386
- const verdict = evaluateWorkspaceModelPolicy(policy, { providerId, modelId: model });
3808
+ const providerId = policyProviderIdForModel(settings, canonicalModel);
3809
+ const verdict = evaluateWorkspaceModelPolicy(policy, {
3810
+ providerId,
3811
+ modelId: canonicalModel
3812
+ });
3387
3813
  if (!verdict.allowed) {
3388
3814
  throw new HTTPException9(422, {
3389
- message: verdict.reason === "provider" ? `model "${model}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${model}" is not allowed by this workspace's model policy`
3815
+ message: verdict.reason === "provider" ? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${canonicalModel}" is not allowed by this workspace's model policy`
3390
3816
  });
3391
3817
  }
3392
3818
  }
@@ -3407,7 +3833,7 @@ function reasoningEffortForSession(metadata, fallback) {
3407
3833
  }
3408
3834
  async function postUserMessageTurn(input) {
3409
3835
  const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
3410
- const requestedModel = input.model ?? null;
3836
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
3411
3837
  const requestedReasoningEffort = input.reasoningEffort ?? null;
3412
3838
  assertConfiguredModel(settings, requestedModel);
3413
3839
  await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
@@ -3437,9 +3863,11 @@ async function postUserMessageTurn(input) {
3437
3863
  turnInstructions: input.turnInstructions ?? null,
3438
3864
  resources: input.resources,
3439
3865
  tools: input.tools,
3866
+ toolsProvided: input.toolsProvided,
3440
3867
  model: requestedModel,
3441
3868
  reasoningEffort: requestedReasoningEffort,
3442
- reasoningEffortFallback: settings.openaiReasoningEffort,
3869
+ reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
3870
+ turnExecutionPolicy: input.turnExecutionPolicy,
3443
3871
  source: input.origin === "operator" ? "api" : "user",
3444
3872
  mcpCredentialUpdates: input.mcpCredentialUpdates ?? []
3445
3873
  })
@@ -3506,6 +3934,16 @@ async function postUserMessageTurn(input) {
3506
3934
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3507
3935
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3508
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
+ }
3509
3947
  const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
3510
3948
  if (parentSessionId) {
3511
3949
  await requireSessionAuthorization(deps, grant, {
@@ -3535,12 +3973,50 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3535
3973
  const resources = normalizeResources(
3536
3974
  hasOwnProperty(rawPayload, "resources") ? payload.resources : parentSession?.resources ?? payload.resources
3537
3975
  );
3976
+ const toolsProvided = hasOwnProperty(rawPayload, "tools");
3538
3977
  const requestedTools = validateToolRefs(
3539
- hasOwnProperty(rawPayload, "tools") ? payload.tools : parentSession?.tools ?? payload.tools,
3978
+ toolsProvided ? payload.tools : parentSession?.tools ?? payload.tools,
3540
3979
  runtimeSettings
3541
3980
  );
3542
- const defaultedTools = hasOwnProperty(rawPayload, "tools") || parentSession ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
3543
- const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
3981
+ let selectedTools;
3982
+ let toolPolicy;
3983
+ if (parentSession) {
3984
+ const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
3985
+ const parentEffective = withFirstPartyTools(
3986
+ parentTracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
3987
+ availableToolRefs(parentSession.tools, runtimeSettings),
3988
+ settings,
3989
+ runtimeSettings
3990
+ ) : parentSession.tools,
3991
+ runtimeSettings
3992
+ );
3993
+ if (toolsProvided) {
3994
+ assertToolRefsSubset(
3995
+ requestedTools,
3996
+ parentEffective,
3997
+ "child tools may only narrow the parent session tool policy"
3998
+ );
3999
+ selectedTools = requestedTools;
4000
+ toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
4001
+ } else {
4002
+ selectedTools = parentEffective;
4003
+ toolPolicy = {
4004
+ mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
4005
+ inheritedFromSessionId: parentSession.id
4006
+ };
4007
+ }
4008
+ } else if (toolsProvided) {
4009
+ selectedTools = requestedTools;
4010
+ toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
4011
+ } else {
4012
+ selectedTools = withDefaultEnabledCapabilityMcpTools(
4013
+ requestedTools,
4014
+ settings,
4015
+ capabilityRuntimeSettings
4016
+ );
4017
+ toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
4018
+ }
4019
+ const tools = withFirstPartyTools(selectedTools, runtimeSettings);
3544
4020
  await validateGitHubRepositorySelection(db, workspaceId, resources);
3545
4021
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
3546
4022
  throw new HTTPException9(503, { message: "object storage is not configured" });
@@ -3568,16 +4044,30 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3568
4044
  frozenRigVersionId = rig.activeVersion.id;
3569
4045
  }
3570
4046
  }
3571
- assertConfiguredModel(settings, payload.model);
3572
- await assertWorkspaceModelPolicyAllows(
3573
- db,
3574
- settings,
3575
- workspaceId,
3576
- payload.model ?? settings.openaiModel
3577
- );
3578
- const model = payload.model ?? settings.openaiModel;
4047
+ const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
4048
+ if (model === null || model === void 0) {
4049
+ throw new Error("effective session model unexpectedly resolved to null");
4050
+ }
4051
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
3579
4052
  const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
3580
- let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? (parentSessionId ? [...new Set(grant.permissions)] : null);
4053
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
4054
+ modelId: model,
4055
+ requestedModelId: payload.model ?? null,
4056
+ modelSource: payload.model === void 0 ? "deployment" : "explicit",
4057
+ reasoningEffort,
4058
+ reasoningSource: payload.reasoningEffort === void 0 ? "deployment" : "explicit"
4059
+ });
4060
+ const parentFirstPartyMcpPermissions = parentSession ? [...parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS] : null;
4061
+ if (parentFirstPartyMcpPermissions && payload.firstPartyMcpPermissions?.some(
4062
+ (permission) => !hasPermission(parentFirstPartyMcpPermissions, permission)
4063
+ )) {
4064
+ throw new HTTPException9(403, {
4065
+ message: "child first-party MCP permissions may only narrow the parent session grant"
4066
+ });
4067
+ }
4068
+ let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? (parentFirstPartyMcpPermissions ? parentFirstPartyMcpPermissions.filter(
4069
+ (permission) => hasPermission(grant.permissions, permission)
4070
+ ) : null);
3581
4071
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
3582
4072
  throw new HTTPException9(422, {
3583
4073
  message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
@@ -3710,9 +4200,11 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3710
4200
  turnInstructions: payload.turnInstructions ?? null,
3711
4201
  resources,
3712
4202
  tools,
4203
+ toolPolicy,
3713
4204
  ...payload.clientEventId ? { clientEventId: payload.clientEventId } : {},
3714
4205
  model,
3715
4206
  reasoningEffort,
4207
+ turnExecutionPolicy,
3716
4208
  // A shared spawn inherits the box's backend; a caller-supplied
3717
4209
  // sandboxBackend on a shared spawn is ignored (it is the same box). A
3718
4210
  // machine-targeted top-level create labels the home "selfhosted"
@@ -3742,11 +4234,18 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3742
4234
  sessionMcpServers: sessionMcpServers.metadata,
3743
4235
  parentSessionId,
3744
4236
  createIdempotencyKey: payload.idempotencyKey ?? null,
4237
+ maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
4238
+ allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
4239
+ subjectId: grant.subjectId,
3745
4240
  // Create-time machine targeting (A-2a): when a target sandbox is named, the
3746
4241
  // active-sandbox pointer is seeded race-free inside createAndStartSession
3747
4242
  // (after the row exists, before the first turn dispatches). Validation
3748
4243
  // (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
3749
- 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
3750
4249
  });
3751
4250
  } catch (error) {
3752
4251
  if (error instanceof AgentCommandAuthorityError) {
@@ -3777,6 +4276,11 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3777
4276
  return session;
3778
4277
  }
3779
4278
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
4279
+ if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
4280
+ throw new HTTPException9(503, {
4281
+ message: "explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry"
4282
+ });
4283
+ }
3780
4284
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3781
4285
  await requireSessionAuthorization(deps, grant, {
3782
4286
  sessionId,
@@ -3789,19 +4293,50 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3789
4293
  settings
3790
4294
  );
3791
4295
  const existingSession = await requireSession2(db, workspaceId, sessionId);
4296
+ const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
4297
+ const effectiveModel = canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
4298
+ if (effectiveModel === null) {
4299
+ throw new Error("effective follow-up model unexpectedly resolved to null");
4300
+ }
4301
+ const sessionReasoningEffort = reasoningEffortForSession(
4302
+ existingSession.metadata,
4303
+ settings.openaiReasoningEffort
4304
+ );
4305
+ const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
4306
+ const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
4307
+ modelId: effectiveModel,
4308
+ requestedModelId: input.model ?? null,
4309
+ modelSource: input.model == null ? "session" : "explicit",
4310
+ reasoningEffort: effectiveReasoningEffort,
4311
+ reasoningSource: input.reasoningEffort == null ? "session" : "explicit"
4312
+ });
3792
4313
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
3793
4314
  capabilityRuntimeSettings,
3794
4315
  existingSession.mcpServers
3795
4316
  );
3796
4317
  const requestedResources = normalizeResources(input.resources ?? []);
3797
- const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
3798
- const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
4318
+ const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
4319
+ const sessionPolicyTools = withFirstPartyTools(
4320
+ tracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
4321
+ availableToolRefs(existingSession.tools, runtimeSettings),
4322
+ settings,
4323
+ capabilityRuntimeSettings
4324
+ ) : existingSession.tools,
4325
+ runtimeSettings
4326
+ );
4327
+ const validatedTools = input.toolsProvided ? validateToolRefsForSessionPolicy({
4328
+ requested: input.tools ?? [],
4329
+ settings: runtimeSettings,
4330
+ allowedTools: sessionPolicyTools,
4331
+ message: "message tools may only narrow the session tool policy"
4332
+ }) : [];
4333
+ const requestedTools = input.toolsProvided ? validatedTools : [];
3799
4334
  await requireLimit(deps, {
3800
4335
  accountId: grant.accountId,
3801
4336
  workspaceId,
3802
4337
  action: "agent_run:create",
3803
4338
  quantity: 1,
3804
- model: input.model ?? existingSession.model
4339
+ model: effectiveModel
3805
4340
  });
3806
4341
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
3807
4342
  throw new HTTPException9(503, { message: "object storage is not configured" });
@@ -3830,8 +4365,11 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
3830
4365
  turnInstructions: input.turnInstructions ?? null,
3831
4366
  resources: requestedResources,
3832
4367
  tools: requestedTools,
4368
+ toolsProvided: input.toolsProvided,
3833
4369
  model: input.model ?? null,
3834
4370
  reasoningEffort: input.reasoningEffort ?? null,
4371
+ reasoningEffortFallback: sessionReasoningEffort,
4372
+ turnExecutionPolicy,
3835
4373
  mcpCredentialUpdates,
3836
4374
  delivery: input.delivery ?? "send",
3837
4375
  origin: delegatedServiceInitiator ? "operator" : input.origin ?? "human",
@@ -3877,7 +4415,7 @@ async function updateSessionTitle(deps, grant, sessionId, title, source) {
3877
4415
  const workspaceId = grant.workspaceId;
3878
4416
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
3879
4417
  if (result.updated) {
3880
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
4418
+ await appendAndPublishEvents2(db, bus, workspaceId, sessionId, [
3881
4419
  {
3882
4420
  type: "session.title_set",
3883
4421
  payload: {
@@ -3892,6 +4430,48 @@ async function updateSessionTitle(deps, grant, sessionId, title, source) {
3892
4430
  relatedSessionAccess: authorization?.relatedSessionAccess ?? "root"
3893
4431
  };
3894
4432
  }
4433
+ async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId, requireApproval) {
4434
+ const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
4435
+ await requireSessionAuthorization(deps, grant, {
4436
+ sessionId,
4437
+ operation: "session.mcp.approval_policy.write",
4438
+ surface: "core"
4439
+ });
4440
+ requirePermission(grant, "sessions:control");
4441
+ const outcome = {};
4442
+ const events = await appendSessionEventsWithLockedSessionUpdate(
4443
+ deps.db,
4444
+ grant.workspaceId,
4445
+ sessionId,
4446
+ async (_session, context) => {
4447
+ const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
4448
+ if (!result.server) {
4449
+ throw new HTTPException9(404, { message: "session MCP server not found" });
4450
+ }
4451
+ outcome.server = result.server;
4452
+ return {
4453
+ events: result.changed ? [
4454
+ {
4455
+ type: "session.mcp.approval_policy.updated",
4456
+ payload: {
4457
+ serverId,
4458
+ effectiveFrom: "next_attempt"
4459
+ }
4460
+ }
4461
+ ] : []
4462
+ };
4463
+ }
4464
+ );
4465
+ const updatedServer = outcome.server;
4466
+ if (!updatedServer) {
4467
+ throw new Error("session MCP approval policy update returned no server");
4468
+ }
4469
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
4470
+ return {
4471
+ server: updatedServer,
4472
+ effectiveFrom: "next_attempt"
4473
+ };
4474
+ }
3895
4475
  async function readSessionLineage(deps, grant, sessionId) {
3896
4476
  const authorization = await requireSessionAuthorization(deps, grant, {
3897
4477
  sessionId,
@@ -4032,6 +4612,7 @@ async function validatedScheduledTaskUpdate(input) {
4032
4612
  settings: input.settings,
4033
4613
  db: input.db,
4034
4614
  objectStorage: input.objectStorage,
4615
+ grant: input.grant,
4035
4616
  workspaceId: input.existing.workspaceId,
4036
4617
  payload: { agentConfig: input.payload.agentConfig },
4037
4618
  ...input.toolsProvided !== void 0 ? { toolsProvided: input.toolsProvided } : {}
@@ -4095,13 +4676,8 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
4095
4676
  return `agent_run.created:scheduled-trigger:${workspaceId}:${taskId}:${triggerToken}`;
4096
4677
  }
4097
4678
  async function validateScheduledTaskAgentConfig(input) {
4098
- assertConfiguredModel(input.settings, input.payload.agentConfig.model);
4099
- await assertWorkspaceModelPolicyAllows(
4100
- input.db,
4101
- input.settings,
4102
- input.workspaceId,
4103
- input.payload.agentConfig.model
4104
- );
4679
+ const model = canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
4680
+ await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
4105
4681
  const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
4106
4682
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
4107
4683
  input.db,
@@ -4119,8 +4695,21 @@ async function validateScheduledTaskAgentConfig(input) {
4119
4695
  throw new HTTPException10(503, { message: "object storage is not configured" });
4120
4696
  }
4121
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
+ }
4122
4710
  return {
4123
4711
  ...input.payload.agentConfig,
4712
+ ...model === void 0 || model === null ? {} : { model },
4124
4713
  prompt,
4125
4714
  resources,
4126
4715
  tools
@@ -4188,6 +4777,102 @@ function assertWorkspaceDeletable(input) {
4188
4777
  }
4189
4778
  }
4190
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
+
4191
4876
  // src/application/session-commands.ts
4192
4877
  import { reasoningEffortForMetadata as reasoningEffortForMetadata2 } from "@opengeni/contracts";
4193
4878
  import {
@@ -4209,7 +4894,7 @@ import {
4209
4894
  steerAgentSessionInTransaction,
4210
4895
  steerQueuedTurnInTransaction,
4211
4896
  withWorkspaceRls,
4212
- withWorkspaceSubjectRls as withWorkspaceSubjectRls2
4897
+ withWorkspaceSubjectRls as withWorkspaceSubjectRls3
4213
4898
  } from "@opengeni/db";
4214
4899
  import {
4215
4900
  publishDurableSessionEvents as publishDurableSessionEvents2,
@@ -4423,6 +5108,7 @@ function composerDraft(row) {
4423
5108
  text: row.text,
4424
5109
  resources: row.resources,
4425
5110
  tools: row.tools,
5111
+ toolsProvided: row.toolsProvided,
4426
5112
  model: row.model,
4427
5113
  reasoningEffort: row.reasoningEffort,
4428
5114
  sourceTurnId: row.sourceTurnId,
@@ -4500,7 +5186,7 @@ async function deleteHumanQueuePrompt(deps, context, turnId, input) {
4500
5186
  }
4501
5187
  async function editHumanQueuePrompt(deps, context, turnId, input) {
4502
5188
  const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
4503
- const result = await withWorkspaceSubjectRls2(
5189
+ const result = await withWorkspaceSubjectRls3(
4504
5190
  deps.db,
4505
5191
  context.workspaceId,
4506
5192
  context.subjectId,
@@ -4622,7 +5308,7 @@ async function controlHumanWorkspace(deps, context, input) {
4622
5308
  }
4623
5309
  async function getHumanComposerDraft(deps, context) {
4624
5310
  await authorizeHumanSessionCommand(deps, context, "session.composer.read");
4625
- const row = await withWorkspaceSubjectRls2(
5311
+ const row = await withWorkspaceSubjectRls3(
4626
5312
  deps.db,
4627
5313
  context.workspaceId,
4628
5314
  context.subjectId,
@@ -4641,6 +5327,7 @@ async function getHumanComposerDraft(deps, context) {
4641
5327
  text: "",
4642
5328
  resources: [],
4643
5329
  tools: [],
5330
+ toolsProvided: false,
4644
5331
  model: session.model,
4645
5332
  reasoningEffort: reasoningEffortForMetadata2(session.metadata, "medium"),
4646
5333
  sourceTurnId: null,
@@ -4650,7 +5337,7 @@ async function getHumanComposerDraft(deps, context) {
4650
5337
  }
4651
5338
  async function saveHumanComposerDraft(deps, context, input) {
4652
5339
  await authorizeHumanSessionCommand(deps, context, "session.composer.write");
4653
- const row = await withWorkspaceSubjectRls2(
5340
+ const row = await withWorkspaceSubjectRls3(
4654
5341
  deps.db,
4655
5342
  context.workspaceId,
4656
5343
  context.subjectId,
@@ -4678,6 +5365,7 @@ export {
4678
5365
  SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
4679
5366
  SessionAuthorizationDeniedError,
4680
5367
  SessionAuthorizationUnavailableError,
5368
+ SessionSpawnDeniedError,
4681
5369
  acceptSessionUserMessage,
4682
5370
  activateRigVersionForApi,
4683
5371
  appendRigSetupCommand,
@@ -4686,12 +5374,15 @@ export {
4686
5374
  assertAllowedVariableSetVariableName,
4687
5375
  assertConfiguredModel,
4688
5376
  assertPackSandboxImageCompatible,
5377
+ assertToolRefsSubset,
4689
5378
  assertWorkspaceDeletable,
4690
5379
  assertWorkspaceMemberRemovable,
4691
5380
  assertWorkspaceModelPolicyAllows,
5381
+ availableToolRefs,
4692
5382
  buildCapabilityCatalog,
4693
5383
  buildFleetContextForSession,
4694
5384
  buildMarketingDailyAnalysisAgentConfig,
5385
+ canonicalConfiguredModel,
4695
5386
  checkLimit,
4696
5387
  classifyRigVerificationOutcome,
4697
5388
  controlAgentSessionWorkstream,
@@ -4710,6 +5401,7 @@ export {
4710
5401
  editHumanQueuePrompt,
4711
5402
  enableCapability,
4712
5403
  enabledCapabilityMcpToolRefs,
5404
+ getActorNewSessionDraft,
4713
5405
  getCapabilityPack,
4714
5406
  getHumanComposerDraft,
4715
5407
  hasPermission,
@@ -4755,15 +5447,19 @@ export {
4755
5447
  requireVariableSetForApi,
4756
5448
  resolveCapabilityPack,
4757
5449
  resolveMemberSubjectId,
5450
+ resolveSessionToolPolicy,
4758
5451
  restoreScheduledTask,
4759
5452
  rigActorForGrant,
4760
5453
  routingEnabled,
4761
5454
  runOnSandbox,
5455
+ saveActorNewSessionDraft,
4762
5456
  saveHumanComposerDraft,
4763
5457
  scheduledTaskTemporalScheduleId,
4764
5458
  scheduledTaskToolsProvided,
4765
5459
  scheduledTaskTriggerToken,
4766
5460
  sendAgentSessionMessage,
5461
+ sessionSpawnDenialEnvelope,
5462
+ sessionWithEffectiveToolPolicy,
4767
5463
  settingsWithEnabledCapabilityMcpServers,
4768
5464
  settingsWithMcpCapabilityServers,
4769
5465
  settingsWithSessionMcpServerMetadata,
@@ -4774,6 +5470,7 @@ export {
4774
5470
  syncCreatedScheduledTask,
4775
5471
  syncUpdatedScheduledTask,
4776
5472
  updateRigForApi,
5473
+ updateSessionMcpApprovalPolicy,
4777
5474
  updateSessionTitle,
4778
5475
  validateFileResources,
4779
5476
  validateGitHubRepositorySelection,
@@ -4781,10 +5478,13 @@ export {
4781
5478
  validateGitHubRepositorySelectionShapes,
4782
5479
  validateMcpCapabilityConnection,
4783
5480
  validateToolRefs,
5481
+ validateToolRefsForSessionPolicy,
4784
5482
  validateVariableSetAttachment,
4785
5483
  validatedScheduledTaskUpdate,
4786
5484
  withDefaultEnabledCapabilityMcpTools,
4787
5485
  workflowIdForSession,
5486
+ workspaceSessionToolPolicyDefaultServerIds,
5487
+ workspaceSessionToolPolicyServerIds,
4788
5488
  wrapChannelABoxWithRouting
4789
5489
  };
4790
5490
  //# sourceMappingURL=index.js.map