@opengeni/core 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,10 +25,12 @@ import { HTTPException } from "hono/http-exception";
25
25
  import {
26
26
  advanceWorkspaceGenerationForDirectRequest,
27
27
  advanceWorkspaceGenerationForRetainedProcess,
28
+ getRetainedProcess,
28
29
  getSandbox,
29
30
  markWarmLeaseInstanceLost,
30
31
  readActiveSandbox,
31
32
  retainWorkspaceMutationProcess,
33
+ retainedProcessSettlementIdentity,
32
34
  settleRetainedProcess,
33
35
  verifyDirectWorkspaceMutationSettlement,
34
36
  verifyRetainedProcessMutationSettlement
@@ -43,7 +45,12 @@ import {
43
45
  function relayConfigFromSettings(settings) {
44
46
  const raw = settings.selfhostedRelayUrl?.trim();
45
47
  if (!raw) {
46
- return { host: "relay.opengeni.local", port: 443, tls: true, path: "/stream" };
48
+ return {
49
+ host: "relay.opengeni.local",
50
+ port: 443,
51
+ tls: true,
52
+ path: "/stream"
53
+ };
47
54
  }
48
55
  try {
49
56
  const url = new URL(raw.includes("://") ? raw : `wss://${raw}`);
@@ -76,7 +83,8 @@ function routingEnabled(settings) {
76
83
  }
77
84
  function wrapChannelABoxWithRouting(services, ids, established) {
78
85
  const { db, settings, bus } = services;
79
- const beforeMutation = async ({
86
+ const homeLease = ids.homeLease;
87
+ const beforeMutation = homeLease ? async ({
80
88
  op,
81
89
  backend
82
90
  }) => {
@@ -92,15 +100,15 @@ function wrapChannelABoxWithRouting(services, ids, established) {
92
100
  sessionId: ids.sessionId,
93
101
  requestId: ids.directRequest.requestId,
94
102
  holderId: ids.directRequest.holderId,
95
- sandboxGroupId: ids.homeLease.sandboxGroupId,
103
+ sandboxGroupId: homeLease.sandboxGroupId,
96
104
  expectedEpoch: backend.leaseEpoch,
97
105
  expectedInstanceId: backend.providerInstanceId,
98
106
  routeTargetId: backend.sandboxId,
99
107
  routeEpoch: backend.activeEpoch,
100
108
  operation: op
101
109
  });
102
- };
103
- const afterMutation = async ({
110
+ } : void 0;
111
+ const afterMutation = homeLease ? async ({
104
112
  op,
105
113
  backend,
106
114
  admission,
@@ -126,7 +134,7 @@ function wrapChannelABoxWithRouting(services, ids, established) {
126
134
  kind: "direct",
127
135
  requestId: ids.directRequest.requestId,
128
136
  holderId: ids.directRequest.holderId,
129
- sandboxGroupId: ids.homeLease.sandboxGroupId,
137
+ sandboxGroupId: homeLease.sandboxGroupId,
130
138
  expectedEpoch: backend.leaseEpoch,
131
139
  expectedInstanceId: backend.providerInstanceId,
132
140
  routeTargetId: exactAdmission.routeTargetId,
@@ -141,7 +149,7 @@ function wrapChannelABoxWithRouting(services, ids, established) {
141
149
  sessionId: ids.sessionId,
142
150
  requestId: ids.directRequest.requestId,
143
151
  holderId: ids.directRequest.holderId,
144
- sandboxGroupId: ids.homeLease.sandboxGroupId,
152
+ sandboxGroupId: homeLease.sandboxGroupId,
145
153
  expectedEpoch: backend.leaseEpoch,
146
154
  expectedInstanceId: backend.providerInstanceId,
147
155
  routeTargetId: exactAdmission.routeTargetId,
@@ -150,8 +158,8 @@ function wrapChannelABoxWithRouting(services, ids, established) {
150
158
  operation: op,
151
159
  outcome
152
160
  });
153
- };
154
- const beforeProcessMutation = async ({
161
+ } : void 0;
162
+ const beforeProcessMutation = homeLease ? async ({
155
163
  op,
156
164
  process
157
165
  }) => await advanceWorkspaceGenerationForRetainedProcess(db, {
@@ -160,8 +168,8 @@ function wrapChannelABoxWithRouting(services, ids, established) {
160
168
  sessionId: ids.sessionId,
161
169
  processId: process.id,
162
170
  operation: op
163
- });
164
- const afterProcessMutation = async ({
171
+ }) : void 0;
172
+ const afterProcessMutation = homeLease ? async ({
165
173
  op,
166
174
  process,
167
175
  admission,
@@ -179,26 +187,37 @@ function wrapChannelABoxWithRouting(services, ids, established) {
179
187
  operation: op,
180
188
  outcome
181
189
  });
182
- };
183
- const settleProcess = async ({
190
+ } : void 0;
191
+ const settleProcess = homeLease ? async ({
184
192
  backend,
185
193
  process,
186
194
  proof
187
195
  }) => {
188
- if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0) {
196
+ if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0 || backend.activeEpoch === void 0) {
189
197
  return;
190
198
  }
199
+ const durable = await getRetainedProcess(db, {
200
+ workspaceId: ids.workspaceId,
201
+ sessionId: ids.sessionId,
202
+ processId: process.id
203
+ });
204
+ if (!durable || durable.providerSessionId !== process.providerSessionId || durable.providerBackend !== backend.kind || durable.providerInstanceId !== backend.providerInstanceId || durable.leaseEpoch !== backend.leaseEpoch || durable.routeKind !== (backend.sandboxId === null ? "home" : "active") || durable.routeTargetId !== backend.sandboxId || durable.routeEpoch !== backend.activeEpoch) {
205
+ throw new Error(
206
+ "API retained-process settlement lost its exact durable backend identity"
207
+ );
208
+ }
191
209
  await settleRetainedProcess(db, {
192
210
  accountId: ids.accountId,
193
211
  workspaceId: ids.workspaceId,
194
212
  sessionId: ids.sessionId,
195
213
  processId: process.id,
214
+ expected: retainedProcessSettlementIdentity(durable),
196
215
  outcome: proof.outcome,
197
216
  exitCode: proof.exitCode,
198
217
  reason: proof.reason,
199
218
  idleGraceMs: settings.sandboxIdleGraceMs
200
219
  });
201
- };
220
+ } : void 0;
202
221
  const resolver = makeActiveBackendResolver({
203
222
  workspaceId: ids.workspaceId,
204
223
  defaultBackend: established.session,
@@ -214,21 +233,34 @@ function wrapChannelABoxWithRouting(services, ids, established) {
214
233
  },
215
234
  controlRpcFactory: controlRpcFactory(bus),
216
235
  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
- })
236
+ selfhostedTimeoutMs: settings.sandboxSelfhostedControlTimeoutMs,
237
+ selfhostedExecTimeoutMs: settings.sandboxSelfhostedExecTimeoutMs,
238
+ ...ids.pinnedSelfhosted ? {
239
+ pinnedSelfhosted: {
240
+ sandboxId: ids.pinnedSelfhosted.sandboxId,
241
+ epoch: ids.pinnedSelfhosted.epoch,
242
+ session: established.session
243
+ }
244
+ } : {},
245
+ ...homeLease ? {
246
+ resolveDefaultBackend: async () => ({
247
+ session: established.session,
248
+ sandboxId: null,
249
+ kind: established.backendId,
250
+ leaseEpoch: homeLease.leaseEpoch,
251
+ providerInstanceId: homeLease.instanceId
252
+ })
253
+ } : {}
224
254
  });
225
255
  const proxy = new RoutingSandboxSession({
226
256
  defaultResolved: {
227
257
  session: established.session,
228
258
  sandboxId: null,
229
259
  kind: established.backendId,
230
- leaseEpoch: ids.homeLease.leaseEpoch,
231
- providerInstanceId: ids.homeLease.instanceId
260
+ ...homeLease ? {
261
+ leaseEpoch: homeLease.leaseEpoch,
262
+ providerInstanceId: homeLease.instanceId
263
+ } : {}
232
264
  },
233
265
  readPointer: async () => {
234
266
  if (!routingEnabled(settings)) {
@@ -238,36 +270,38 @@ function wrapChannelABoxWithRouting(services, ids, established) {
238
270
  return pointer ?? { activeSandboxId: null, activeEpoch: 0 };
239
271
  },
240
272
  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);
273
+ ...beforeMutation ? { beforeMutation } : {},
274
+ ...afterMutation ? { afterMutation } : {},
275
+ ...beforeProcessMutation ? { beforeProcessMutation } : {},
276
+ ...afterProcessMutation ? { afterProcessMutation } : {},
277
+ ...settleProcess ? { settleProcess } : {},
278
+ ...homeLease ? {
279
+ onDefaultBackendError: async ({ error }) => {
280
+ if (!isProviderSandboxGoneDuringRoutedOperation(homeLease.backend, error)) return null;
281
+ const marked = await markWarmLeaseInstanceLost(db, {
282
+ accountId: ids.accountId,
283
+ workspaceId: ids.workspaceId,
284
+ sandboxGroupId: homeLease.sandboxGroupId,
285
+ expectedEpoch: homeLease.leaseEpoch,
286
+ expectedInstanceId: homeLease.instanceId,
287
+ diagnostic: "provider_not_found_during_routed_operation"
288
+ });
289
+ if (marked.status === "marked" && bus) {
290
+ await appendAndPublishEvents(db, bus, ids.workspaceId, ids.sessionId, [
291
+ {
292
+ type: "sandbox.box.lost",
293
+ payload: { sandboxId: homeLease.instanceId }
294
+ }
295
+ ]).catch(() => void 0);
296
+ }
297
+ const lease = marked.lease;
298
+ const restore = lease?.recovery.restore.status;
299
+ return {
300
+ leaseEpoch: lease?.leaseEpoch ?? homeLease.leaseEpoch,
301
+ recovery: marked.status === "stale" ? "superseded" : restore === "pending" ? "pending" : restore === "degraded" ? "degraded" : "unrecoverable"
302
+ };
263
303
  }
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
- }
304
+ } : {}
271
305
  });
272
306
  return { ...established, session: proxy };
273
307
  }
@@ -3102,6 +3136,9 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
3102
3136
  }
3103
3137
  }
3104
3138
  }
3139
+ function isAuthoritativeGitHubRepositorySelectionError(error) {
3140
+ return error instanceof HTTPException8 && error.status === 422;
3141
+ }
3105
3142
  async function validateFileResources(db, workspaceId, resources) {
3106
3143
  const fileIds = /* @__PURE__ */ new Set();
3107
3144
  for (const resource of resources) {
@@ -3202,11 +3239,9 @@ function resolveSessionToolPolicy(input) {
3202
3239
  const configuredIds = effectiveIds.filter((id) => availableIds.has(id));
3203
3240
  const configuredIdSet = new Set(configuredIds);
3204
3241
  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
- ) : [];
3242
+ const deferredIds = sortedIds(
3243
+ toolRefs.filter((tool) => configuredIdSet.has(tool.id) && !mandatoryIdSet.has(tool.id)).map((tool) => tool.id)
3244
+ );
3210
3245
  const selectedIds = sortedIds(
3211
3246
  selectedRefs.filter(
3212
3247
  (tool) => !mandatoryIdSet.has(tool.id) && !(tracksWorkspaceDefaults && tool.optional === true)
@@ -3229,7 +3264,7 @@ function resolveSessionToolPolicy(input) {
3229
3264
  effectiveIds: projections.effective.ids,
3230
3265
  mandatoryIds: projections.mandatory.ids,
3231
3266
  lazyRouter: {
3232
- state: tracksWorkspaceDefaults ? "required" : "disabled",
3267
+ state: deferredIds.length > 0 ? "required" : "disabled",
3233
3268
  deferredIds: projections.deferred.ids
3234
3269
  },
3235
3270
  configuredIds: projections.configured.ids,
@@ -3246,6 +3281,9 @@ function resolveSessionToolPolicy(input) {
3246
3281
  }
3247
3282
  };
3248
3283
  }
3284
+ function sessionToolPolicyAllowsDefaultNativeTools(policy) {
3285
+ return policy.mode === "workspace_default" && policy.lazyRouter.state === "required";
3286
+ }
3249
3287
  async function workspaceSessionToolPolicyServerIds(db, workspaceId, settings) {
3250
3288
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3251
3289
  return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
@@ -3271,6 +3309,7 @@ function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDe
3271
3309
  }
3272
3310
 
3273
3311
  // src/domain/scheduled-tasks.ts
3312
+ import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY as OPENGENI_SLACK_BOT_SESSION_METADATA_KEY3 } from "@opengeni/contracts";
3274
3313
  import {
3275
3314
  createScheduledTask,
3276
3315
  deleteScheduledTask,
@@ -3280,7 +3319,7 @@ import {
3280
3319
  requireWorkspace as requireWorkspace2,
3281
3320
  updateScheduledTask
3282
3321
  } from "@opengeni/db";
3283
- import { HTTPException as HTTPException10 } from "hono/http-exception";
3322
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
3284
3323
 
3285
3324
  // src/domain/sessions.ts
3286
3325
  import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
@@ -3293,11 +3332,13 @@ import {
3293
3332
  import {
3294
3333
  CreateSessionRequest,
3295
3334
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
3335
+ OPENGENI_SLACK_BOT_SESSION_METADATA_KEY as OPENGENI_SLACK_BOT_SESSION_METADATA_KEY2,
3296
3336
  SessionSpawnDenial,
3297
3337
  ServiceTurnInitiator,
3298
3338
  ServiceTurnInitiatorContext,
3299
3339
  evaluateWorkspaceModelPolicy,
3300
3340
  reasoningEffortForMetadata,
3341
+ stableJson as stableJson2,
3301
3342
  SessionMcpApprovalPolicy
3302
3343
  } from "@opengeni/contracts";
3303
3344
  import {
@@ -3330,17 +3371,80 @@ import {
3330
3371
  QueueCommandConflictError,
3331
3372
  AgentCommandAuthorityError,
3332
3373
  SessionSpawnDeniedDbError,
3333
- SessionControlConflictError
3374
+ SessionControlConflictError,
3375
+ SessionToolPolicyVersionConflictError
3334
3376
  } from "@opengeni/db";
3335
3377
  import {
3336
3378
  appendAndPublishEvents as appendAndPublishEvents2,
3337
3379
  publishDurableSessionEvents,
3338
3380
  publishDurableWorkspaceControlEvent
3339
3381
  } from "@opengeni/events";
3382
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
3383
+
3384
+ // src/domain/slack-bot.ts
3385
+ import {
3386
+ OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
3387
+ OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
3388
+ OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
3389
+ OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
3390
+ OpenGeniSlackBotConnectionMetadata
3391
+ } from "@opengeni/contracts";
3392
+ import {
3393
+ getConnectionMetadata as getConnectionMetadata2
3394
+ } from "@opengeni/db";
3340
3395
  import { HTTPException as HTTPException9 } from "hono/http-exception";
3396
+ function openGeniSlackBotMetadata(metadata) {
3397
+ const parsed = OpenGeniSlackBotConnectionMetadata.safeParse(metadata);
3398
+ return parsed.success ? parsed.data : null;
3399
+ }
3400
+ function isOpenGeniSlackBotConnection(connection) {
3401
+ const granted = new Set(connection.grantedScopes);
3402
+ return connection.verifiedInstallAt != null && connection.verifiedInstallVersion === connection.version && connection.subjectId === null && connection.providerDomain === "slack.com" && connection.kind === "app_install" && granted.size === OPENGENI_SLACK_BOT_REQUIRED_SCOPES.length && OPENGENI_SLACK_BOT_REQUIRED_SCOPES.every((scope) => granted.has(scope)) && openGeniSlackBotMetadata(connection.metadata)?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE;
3403
+ }
3404
+ function hasReservedOpenGeniSlackBotMetadata(metadata) {
3405
+ return metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE || metadata?.credentialLabel === OPENGENI_SLACK_BOT_CREDENTIAL_LABEL;
3406
+ }
3407
+ function hasReservedOpenGeniSlackBotSessionMetadata(metadata) {
3408
+ return Boolean(
3409
+ metadata && Object.prototype.hasOwnProperty.call(metadata, OPENGENI_SLACK_BOT_SESSION_METADATA_KEY)
3410
+ );
3411
+ }
3412
+ async function requireOpenGeniSlackBotConnection(db, workspaceId, connectionId) {
3413
+ const connection = await getConnectionMetadata2(db, workspaceId, connectionId, null);
3414
+ if (!connection || !isOpenGeniSlackBotConnection(connection)) {
3415
+ throw new HTTPException9(422, {
3416
+ message: "slackBotConnectionId must reference an OpenGeni Slack bot connection"
3417
+ });
3418
+ }
3419
+ if (connection.status !== "active") {
3420
+ throw new HTTPException9(422, {
3421
+ message: `OpenGeni Slack bot connection is not active (${connection.status})`
3422
+ });
3423
+ }
3424
+ return connection;
3425
+ }
3426
+ async function validateOpenGeniSlackBotConnectionSelection(db, grant, workspaceId, connectionId) {
3427
+ requirePermission(grant, "connections:read");
3428
+ return await requireOpenGeniSlackBotConnection(db, workspaceId, connectionId);
3429
+ }
3430
+ function scheduledSlackBotConnectionId(metadata) {
3431
+ const value = metadata?.[OPENGENI_SLACK_BOT_SESSION_METADATA_KEY];
3432
+ return typeof value === "string" && zUuid(value) ? value : null;
3433
+ }
3434
+ function isTrustedScheduledSlackBotSession(session) {
3435
+ const scheduledTaskId = session.metadata.scheduledTaskId;
3436
+ const scheduledTaskRunId = session.metadata.scheduledTaskRunId;
3437
+ return session.createdBy?.kind === "service" && session.createdBy.subjectId === "scheduler" && typeof scheduledTaskId === "string" && zUuid(scheduledTaskId) && typeof scheduledTaskRunId === "string" && zUuid(scheduledTaskRunId) && session.createdByContext.scheduledTaskId === scheduledTaskId && session.createdByContext.scheduledTaskRunId === scheduledTaskRunId && scheduledSlackBotConnectionId(session.metadata) !== null;
3438
+ }
3439
+ function zUuid(value) {
3440
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
3441
+ }
3442
+
3443
+ // src/domain/sessions.ts
3341
3444
  var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
3342
3445
  var maxSessionMcpCredentialHeaders = 16;
3343
3446
  var maxSessionMcpCredentialHeaderValueLength = 4096;
3447
+ var maxToolPolicyAuditRefs = 40;
3344
3448
  var sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
3345
3449
  var SessionSpawnDeniedError = class extends Error {
3346
3450
  denial;
@@ -3368,7 +3472,7 @@ function sessionSpawnDenialEnvelope(error) {
3368
3472
  function serviceInitiatorForGrant(grant) {
3369
3473
  if (!grant.serviceInitiator) {
3370
3474
  if (grant.serviceInitiatorContext) {
3371
- throw new HTTPException9(403, {
3475
+ throw new HTTPException10(403, {
3372
3476
  message: "service initiator context requires a signed service initiator"
3373
3477
  });
3374
3478
  }
@@ -3376,13 +3480,13 @@ function serviceInitiatorForGrant(grant) {
3376
3480
  }
3377
3481
  const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
3378
3482
  if (!initiator.success) {
3379
- throw new HTTPException9(403, {
3483
+ throw new HTTPException10(403, {
3380
3484
  message: "a delegated command initiator must be a bounded service principal"
3381
3485
  });
3382
3486
  }
3383
3487
  const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
3384
3488
  if (!context.success) {
3385
- throw new HTTPException9(403, {
3489
+ throw new HTTPException10(403, {
3386
3490
  message: "delegated service initiator context is invalid or reserved"
3387
3491
  });
3388
3492
  }
@@ -3390,7 +3494,7 @@ function serviceInitiatorForGrant(grant) {
3390
3494
  const callerAttemptId = grant.metadata?.["attemptId"];
3391
3495
  const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
3392
3496
  if (callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0) {
3393
- throw new HTTPException9(403, {
3497
+ throw new HTTPException10(403, {
3394
3498
  message: "a service initiator cannot replace an exact agent-attempt initiator"
3395
3499
  });
3396
3500
  }
@@ -3408,7 +3512,7 @@ function creationInitiatorForGrant(grant) {
3408
3512
  const hasCallerTurnClaim = callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0;
3409
3513
  if (hasCallerTurnClaim) {
3410
3514
  if (typeof callerSessionId !== "string" || typeof callerTurnId !== "string" || typeof callerAttemptId !== "string" || typeof callerExecutionGeneration !== "number" || !Number.isSafeInteger(callerExecutionGeneration) || callerExecutionGeneration < 1) {
3411
- throw new HTTPException9(403, { message: "caller attempt claims are incomplete" });
3515
+ throw new HTTPException10(403, { message: "caller attempt claims are incomplete" });
3412
3516
  }
3413
3517
  const actor = {
3414
3518
  type: "agent_attempt",
@@ -3437,27 +3541,27 @@ function normalizedSessionMcpCredentialHeaders(headers) {
3437
3541
  }
3438
3542
  const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
3439
3543
  if (entries.length > maxSessionMcpCredentialHeaders) {
3440
- throw new HTTPException9(422, {
3544
+ throw new HTTPException10(422, {
3441
3545
  message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
3442
3546
  });
3443
3547
  }
3444
3548
  const seen = /* @__PURE__ */ new Set();
3445
3549
  for (const [name, value] of entries) {
3446
3550
  if (!sessionMcpCredentialHeaderName.test(name)) {
3447
- throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
3551
+ throw new HTTPException10(422, { message: `invalid credential header name: ${name}` });
3448
3552
  }
3449
3553
  const lower = name.toLowerCase();
3450
3554
  if (seen.has(lower)) {
3451
- throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
3555
+ throw new HTTPException10(422, { message: `duplicate credential header name: ${name}` });
3452
3556
  }
3453
3557
  seen.add(lower);
3454
3558
  if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
3455
- throw new HTTPException9(422, {
3559
+ throw new HTTPException10(422, {
3456
3560
  message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
3457
3561
  });
3458
3562
  }
3459
3563
  if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
3460
- throw new HTTPException9(422, {
3564
+ throw new HTTPException10(422, {
3461
3565
  message: `credential header ${name} contains forbidden control characters`
3462
3566
  });
3463
3567
  }
@@ -3524,11 +3628,11 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
3524
3628
  const metadata = [];
3525
3629
  for (const server of servers) {
3526
3630
  if (seenIds.has(server.id)) {
3527
- throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
3631
+ throw new HTTPException10(422, { message: `duplicate session MCP server id: ${server.id}` });
3528
3632
  }
3529
3633
  seenIds.add(server.id);
3530
3634
  if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
3531
- throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
3635
+ throw new HTTPException10(422, { message: `MCP server id already exists: ${server.id}` });
3532
3636
  }
3533
3637
  const headers = normalizedSessionMcpCredentialHeaders(server.headers);
3534
3638
  const headersEncrypted = Object.fromEntries(
@@ -3568,13 +3672,13 @@ function validateInheritedSessionMcpServersForCreate(servers) {
3568
3672
  const seenIds = /* @__PURE__ */ new Set();
3569
3673
  for (const server of servers) {
3570
3674
  if (seenIds.has(server.id)) {
3571
- throw new HTTPException9(422, {
3675
+ throw new HTTPException10(422, {
3572
3676
  message: `duplicate inherited session MCP server id: ${server.id}`
3573
3677
  });
3574
3678
  }
3575
3679
  seenIds.add(server.id);
3576
3680
  if (reservedSessionMcpServerIds.has(server.id)) {
3577
- throw new HTTPException9(422, {
3681
+ throw new HTTPException10(422, {
3578
3682
  message: `reserved inherited session MCP server id: ${server.id}`
3579
3683
  });
3580
3684
  }
@@ -3606,13 +3710,13 @@ function validateSessionMcpCredentialUpdates(input) {
3606
3710
  const seenIds = /* @__PURE__ */ new Set();
3607
3711
  const encryptedUpdates = input.updates.map((update) => {
3608
3712
  if (seenIds.has(update.id)) {
3609
- throw new HTTPException9(422, {
3713
+ throw new HTTPException10(422, {
3610
3714
  message: `duplicate session MCP credential update id: ${update.id}`
3611
3715
  });
3612
3716
  }
3613
3717
  seenIds.add(update.id);
3614
3718
  if (!knownIds.has(update.id)) {
3615
- throw new HTTPException9(422, { message: `unknown session MCP server id: ${update.id}` });
3719
+ throw new HTTPException10(422, { message: `unknown session MCP server id: ${update.id}` });
3616
3720
  }
3617
3721
  const headers = normalizedSessionMcpCredentialHeaders(update.headers);
3618
3722
  return {
@@ -3716,7 +3820,7 @@ async function createAndStartSession(input) {
3716
3820
  async function finishStartSession(input, session) {
3717
3821
  if (input.seedTargetSandbox) {
3718
3822
  if (session.sandboxBackend === "none") {
3719
- throw new HTTPException9(422, {
3823
+ throw new HTTPException10(422, {
3720
3824
  message: "cannot target a machine for a session with no sandbox (backend: none)"
3721
3825
  });
3722
3826
  }
@@ -3736,7 +3840,7 @@ async function finishStartSession(input, session) {
3736
3840
  input.seedTargetSandbox.workingDir ?? null
3737
3841
  );
3738
3842
  if (!seeded.swapped) {
3739
- throw new HTTPException9(422, {
3843
+ throw new HTTPException10(422, {
3740
3844
  message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
3741
3845
  });
3742
3846
  }
@@ -3788,7 +3892,7 @@ function canonicalConfiguredModel(settings, model) {
3788
3892
  if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
3789
3893
  return canonicalModel;
3790
3894
  }
3791
- throw new HTTPException9(422, { message: `model is not available: ${model}` });
3895
+ throw new HTTPException10(422, { message: `model is not available: ${model}` });
3792
3896
  }
3793
3897
  function assertConfiguredModel(settings, model) {
3794
3898
  canonicalConfiguredModel(settings, model);
@@ -3811,7 +3915,7 @@ async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model
3811
3915
  modelId: canonicalModel
3812
3916
  });
3813
3917
  if (!verdict.allowed) {
3814
- throw new HTTPException9(422, {
3918
+ throw new HTTPException10(422, {
3815
3919
  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`
3816
3920
  });
3817
3921
  }
@@ -3819,10 +3923,10 @@ async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model
3819
3923
  async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
3820
3924
  const turn = await getSessionTurn(db, workspaceId, turnId);
3821
3925
  if (!turn || turn.sessionId !== sessionId) {
3822
- throw new HTTPException9(404, { message: "session turn not found" });
3926
+ throw new HTTPException10(404, { message: "session turn not found" });
3823
3927
  }
3824
3928
  if (turn.status !== "queued") {
3825
- throw new HTTPException9(409, {
3929
+ throw new HTTPException10(409, {
3826
3930
  message: `turn is ${turn.status}; only queued turns can be changed`
3827
3931
  });
3828
3932
  }
@@ -3875,13 +3979,13 @@ async function postUserMessageTurn(input) {
3875
3979
  );
3876
3980
  } catch (error) {
3877
3981
  if (error instanceof QueueCommandConflictError || error instanceof SessionControlConflictError) {
3878
- throw new HTTPException9(409, { message: error.message });
3982
+ throw new HTTPException10(409, { message: error.message });
3879
3983
  }
3880
3984
  if (error instanceof Error && error.message.includes("cancelled")) {
3881
- throw new HTTPException9(409, { message: error.message });
3985
+ throw new HTTPException10(409, { message: error.message });
3882
3986
  }
3883
3987
  if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
3884
- throw new HTTPException9(422, { message: error.message });
3988
+ throw new HTTPException10(422, { message: error.message });
3885
3989
  }
3886
3990
  throw error;
3887
3991
  }
@@ -3934,6 +4038,11 @@ async function postUserMessageTurn(input) {
3934
4038
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3935
4039
  const { settings, db, bus, workflowClient, objectStorage } = deps;
3936
4040
  const payload = CreateSessionRequest.parse(rawPayload);
4041
+ if (hasReservedOpenGeniSlackBotSessionMetadata(payload.metadata)) {
4042
+ throw new HTTPException10(422, {
4043
+ message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY2} is reserved for scheduler routing`
4044
+ });
4045
+ }
3937
4046
  if (payload.idempotencyKey) {
3938
4047
  const denial = await getSessionSpawnDenialByIdempotencyKey(
3939
4048
  db,
@@ -3954,7 +4063,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
3954
4063
  }
3955
4064
  const parentSession = parentSessionId ? await getSession2(db, workspaceId, parentSessionId) : null;
3956
4065
  if (parentSessionId && !parentSession) {
3957
- throw new HTTPException9(404, {
4066
+ throw new HTTPException10(404, {
3958
4067
  message: `parent session not found in workspace: ${parentSessionId}`
3959
4068
  });
3960
4069
  }
@@ -4019,7 +4128,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4019
4128
  const tools = withFirstPartyTools(selectedTools, runtimeSettings);
4020
4129
  await validateGitHubRepositorySelection(db, workspaceId, resources);
4021
4130
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
4022
- throw new HTTPException9(503, { message: "object storage is not configured" });
4131
+ throw new HTTPException10(503, { message: "object storage is not configured" });
4023
4132
  }
4024
4133
  await validateFileResources(db, workspaceId, resources);
4025
4134
  const variableSet = payload.variableSetId ? await validateVariableSetAttachment(
@@ -4035,7 +4144,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4035
4144
  const rig = await getRig2(db, workspaceId, requestedRigId);
4036
4145
  if (!rig || !rig.activeVersion) {
4037
4146
  if (payload.rigId) {
4038
- throw new HTTPException9(422, {
4147
+ throw new HTTPException10(422, {
4039
4148
  message: rig ? `rig ${payload.rigId} has no active version to bind` : `unknown rigId: ${payload.rigId}`
4040
4149
  });
4041
4150
  }
@@ -4061,7 +4170,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4061
4170
  if (parentFirstPartyMcpPermissions && payload.firstPartyMcpPermissions?.some(
4062
4171
  (permission) => !hasPermission(parentFirstPartyMcpPermissions, permission)
4063
4172
  )) {
4064
- throw new HTTPException9(403, {
4173
+ throw new HTTPException10(403, {
4065
4174
  message: "child first-party MCP permissions may only narrow the parent session grant"
4066
4175
  });
4067
4176
  }
@@ -4069,19 +4178,19 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4069
4178
  (permission) => hasPermission(grant.permissions, permission)
4070
4179
  ) : null);
4071
4180
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
4072
- throw new HTTPException9(422, {
4181
+ throw new HTTPException10(422, {
4073
4182
  message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
4074
4183
  });
4075
4184
  }
4076
4185
  for (const permission of firstPartyMcpPermissions ?? []) {
4077
4186
  if (!hasPermission(grant.permissions, permission)) {
4078
- throw new HTTPException9(403, {
4187
+ throw new HTTPException10(403, {
4079
4188
  message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
4080
4189
  });
4081
4190
  }
4082
4191
  }
4083
4192
  if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
4084
- throw new HTTPException9(422, {
4193
+ throw new HTTPException10(422, {
4085
4194
  message: "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set"
4086
4195
  });
4087
4196
  }
@@ -4093,7 +4202,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4093
4202
  const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
4094
4203
  if (sandboxChoice === "shared") {
4095
4204
  if (!parentSessionId) {
4096
- throw new HTTPException9(422, {
4205
+ throw new HTTPException10(422, {
4097
4206
  message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
4098
4207
  });
4099
4208
  }
@@ -4116,7 +4225,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4116
4225
  }
4117
4226
  if (variableSetMismatch || rigMismatch) {
4118
4227
  if (payload.sandbox === "shared") {
4119
- throw new HTTPException9(422, {
4228
+ throw new HTTPException10(422, {
4120
4229
  message: variableSetMismatch ? "sandbox:'shared' requires the same variableSet / same environment as the creator's box (the box variable set/environment is fixed at creation); omit sandbox or pass 'new' when attaching a different variableSet/environment." : "sandbox:'shared' requires the same rig as the creator's box (the box's rig setup is fixed at creation); omit sandbox or pass 'new' when binding a different rig."
4121
4230
  });
4122
4231
  }
@@ -4127,7 +4236,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4127
4236
  } else if (typeof sandboxChoice === "object") {
4128
4237
  const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
4129
4238
  if (!member) {
4130
- throw new HTTPException9(404, {
4239
+ throw new HTTPException10(404, {
4131
4240
  message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
4132
4241
  });
4133
4242
  }
@@ -4140,7 +4249,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4140
4249
  if (!memberVariableSetIds.every(
4141
4250
  (memberVariableSetId) => variableSetMatchesGroup(memberVariableSetId)
4142
4251
  )) {
4143
- throw new HTTPException9(422, {
4252
+ throw new HTTPException10(422, {
4144
4253
  message: `sandbox group ${sandboxChoice.groupId} runs a different variableSet / different environment (the box variable set/environment is fixed at creation); create with the group's variableSet/environment or omit sandbox for an own box.`
4145
4254
  });
4146
4255
  }
@@ -4152,7 +4261,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4152
4261
  if (!memberRigVersionIds.every(
4153
4262
  (memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
4154
4263
  )) {
4155
- throw new HTTPException9(422, {
4264
+ throw new HTTPException10(422, {
4156
4265
  message: `sandbox group ${sandboxChoice.groupId} runs a different rig (the box's rig setup is fixed at creation); create with the group's rig or omit sandbox for an own box.`
4157
4266
  });
4158
4267
  }
@@ -4161,7 +4270,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4161
4270
  inheritedBackend = member.sandboxBackend;
4162
4271
  }
4163
4272
  if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
4164
- throw new HTTPException9(422, {
4273
+ throw new HTTPException10(422, {
4165
4274
  message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
4166
4275
  });
4167
4276
  }
@@ -4249,10 +4358,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4249
4358
  });
4250
4359
  } catch (error) {
4251
4360
  if (error instanceof AgentCommandAuthorityError) {
4252
- throw new HTTPException9(403, { message: error.message });
4361
+ throw new HTTPException10(403, { message: error.message });
4253
4362
  }
4254
4363
  if (error instanceof SessionIdConflictError) {
4255
- throw new HTTPException9(409, {
4364
+ throw new HTTPException10(409, {
4256
4365
  message: "requested session id is already in use"
4257
4366
  });
4258
4367
  }
@@ -4277,7 +4386,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
4277
4386
  }
4278
4387
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
4279
4388
  if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
4280
- throw new HTTPException9(503, {
4389
+ throw new HTTPException10(503, {
4281
4390
  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
4391
  });
4283
4392
  }
@@ -4339,7 +4448,7 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
4339
4448
  model: effectiveModel
4340
4449
  });
4341
4450
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
4342
- throw new HTTPException9(503, { message: "object storage is not configured" });
4451
+ throw new HTTPException10(503, { message: "object storage is not configured" });
4343
4452
  }
4344
4453
  await validateFileResources(db, workspaceId, requestedResources);
4345
4454
  await validateGitHubRepositorySelection(db, workspaceId, [
@@ -4446,7 +4555,7 @@ async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId,
4446
4555
  async (_session, context) => {
4447
4556
  const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
4448
4557
  if (!result.server) {
4449
- throw new HTTPException9(404, { message: "session MCP server not found" });
4558
+ throw new HTTPException10(404, { message: "session MCP server not found" });
4450
4559
  }
4451
4560
  outcome.server = result.server;
4452
4561
  return {
@@ -4472,6 +4581,150 @@ async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId,
4472
4581
  effectiveFrom: "next_attempt"
4473
4582
  };
4474
4583
  }
4584
+ function toolPolicyAuditSnapshot(session, tools, policy = session.toolPolicy ?? { mode: "legacy", inheritedFromSessionId: null }) {
4585
+ const allToolRefs = mergeToolRefs([], tools).sort((left, right) => {
4586
+ const leftMandatory = left.kind === "mcp" && left.id === "opengeni";
4587
+ const rightMandatory = right.kind === "mcp" && right.id === "opengeni";
4588
+ if (leftMandatory !== rightMandatory) return leftMandatory ? -1 : 1;
4589
+ return `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`);
4590
+ }).map((tool) => ({
4591
+ kind: tool.kind,
4592
+ id: tool.id,
4593
+ ...tool.optional === void 0 ? {} : { optional: tool.optional }
4594
+ }));
4595
+ const toolRefs = allToolRefs.slice(0, maxToolPolicyAuditRefs);
4596
+ return {
4597
+ mode: policy.mode,
4598
+ inheritedFromSessionId: policy.inheritedFromSessionId,
4599
+ // IDs only: no MCP URLs, names, headers, credentials, schemas, or args.
4600
+ toolIds: [...toolRefs].sort((left, right) => `${left.kind}:${left.id}`.localeCompare(`${right.kind}:${right.id}`)).map((tool) => tool.id),
4601
+ toolRefs,
4602
+ toolCount: allToolRefs.length,
4603
+ truncated: allToolRefs.length > toolRefs.length
4604
+ };
4605
+ }
4606
+ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
4607
+ await requireSessionAuthorization(deps, grant, {
4608
+ sessionId,
4609
+ operation: "session.tool_policy.write",
4610
+ surface: "core"
4611
+ });
4612
+ requirePermission(grant, "sessions:control");
4613
+ const existingSession = await requireSession2(deps.db, grant.workspaceId, sessionId);
4614
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
4615
+ deps.db,
4616
+ grant.workspaceId,
4617
+ deps.settings
4618
+ );
4619
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
4620
+ capabilityRuntimeSettings,
4621
+ existingSession.mcpServers
4622
+ );
4623
+ const explicitRequest = request.mode === "workspace_default" ? null : request;
4624
+ const requestedMode = explicitRequest ? "explicit" : "workspace_default";
4625
+ const explicitRequestedTools = explicitRequest ? (() => {
4626
+ const validatedTools = validateToolRefs(explicitRequest.tools, runtimeSettings);
4627
+ const validatedIds = new Set(validatedTools.map((tool) => `${tool.kind}:${tool.id}`));
4628
+ const unknown = explicitRequest.tools.find(
4629
+ (tool) => !validatedIds.has(`${tool.kind}:${tool.id}`)
4630
+ );
4631
+ if (unknown) {
4632
+ throw new HTTPException10(422, { message: `unknown MCP server id: ${unknown.id}` });
4633
+ }
4634
+ return withFirstPartyTools(validatedTools, runtimeSettings);
4635
+ })() : null;
4636
+ const workspaceDefaultTools = withFirstPartyTools(
4637
+ withDefaultEnabledCapabilityMcpTools([], deps.settings, capabilityRuntimeSettings),
4638
+ runtimeSettings
4639
+ );
4640
+ const events = await appendSessionEventsWithLockedSessionUpdate(
4641
+ deps.db,
4642
+ grant.workspaceId,
4643
+ sessionId,
4644
+ async (session, context) => {
4645
+ const currentVersion = session.toolPolicyVersion ?? 1;
4646
+ if (request.expectedVersion !== currentVersion) {
4647
+ throw new SessionToolPolicyVersionConflictError(currentVersion);
4648
+ }
4649
+ let nextTools;
4650
+ let nextPolicy;
4651
+ if (session.parentSessionId) {
4652
+ const parent = await context.getLockedSession(session.parentSessionId);
4653
+ if (!parent) {
4654
+ throw new HTTPException10(409, { message: "parent session is no longer available" });
4655
+ }
4656
+ const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
4657
+ const parentEffective = withFirstPartyTools(
4658
+ parentTracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
4659
+ availableToolRefs(parent.tools, runtimeSettings),
4660
+ deps.settings,
4661
+ runtimeSettings
4662
+ ) : parent.tools,
4663
+ runtimeSettings
4664
+ );
4665
+ if (requestedMode === "workspace_default") {
4666
+ if (!parentTracksWorkspaceDefaults) {
4667
+ throw new HTTPException10(403, {
4668
+ message: "a child may adopt workspace defaults only while its parent tracks workspace defaults"
4669
+ });
4670
+ }
4671
+ nextTools = parentEffective;
4672
+ nextPolicy = {
4673
+ mode: "workspace_default",
4674
+ inheritedFromSessionId: parent.id
4675
+ };
4676
+ } else {
4677
+ nextTools = explicitRequestedTools;
4678
+ assertToolRefsSubset(
4679
+ nextTools,
4680
+ parentEffective,
4681
+ "session tools may only narrow the parent session tool policy"
4682
+ );
4683
+ nextPolicy = {
4684
+ mode: "explicit",
4685
+ inheritedFromSessionId: parent.id
4686
+ };
4687
+ }
4688
+ } else {
4689
+ nextTools = requestedMode === "workspace_default" ? workspaceDefaultTools : explicitRequestedTools;
4690
+ nextPolicy = { mode: requestedMode, inheritedFromSessionId: null };
4691
+ }
4692
+ const currentPolicy = session.toolPolicy ?? {
4693
+ mode: "legacy",
4694
+ inheritedFromSessionId: null
4695
+ };
4696
+ const unchanged = stableJson2({ tools: session.tools, policy: currentPolicy }) === stableJson2({ tools: nextTools, policy: nextPolicy });
4697
+ if (unchanged) {
4698
+ return { events: [] };
4699
+ }
4700
+ const nextVersion = currentVersion + 1;
4701
+ return {
4702
+ events: [
4703
+ {
4704
+ type: "session.tool_policy.updated",
4705
+ payload: {
4706
+ before: toolPolicyAuditSnapshot(session, session.tools, currentPolicy),
4707
+ after: toolPolicyAuditSnapshot(session, nextTools, nextPolicy),
4708
+ version: nextVersion,
4709
+ effectiveFrom: "next_attempt"
4710
+ }
4711
+ }
4712
+ ],
4713
+ update: {
4714
+ tools: nextTools,
4715
+ toolPolicy: nextPolicy,
4716
+ toolPolicyVersion: nextVersion,
4717
+ expectedToolPolicyVersion: request.expectedVersion
4718
+ }
4719
+ };
4720
+ },
4721
+ { lockParentSession: true }
4722
+ );
4723
+ if (events.length > 0) {
4724
+ await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
4725
+ }
4726
+ return await requireSession2(deps.db, grant.workspaceId, sessionId);
4727
+ }
4475
4728
  async function readSessionLineage(deps, grant, sessionId) {
4476
4729
  const authorization = await requireSessionAuthorization(deps, grant, {
4477
4730
  sessionId,
@@ -4481,13 +4734,13 @@ async function readSessionLineage(deps, grant, sessionId) {
4481
4734
  if (authorization?.relatedSessionAccess === "target") {
4482
4735
  const session = await getSession2(deps.db, grant.workspaceId, sessionId);
4483
4736
  if (!session) {
4484
- throw new HTTPException9(404, { message: "session not found" });
4737
+ throw new HTTPException10(404, { message: "session not found" });
4485
4738
  }
4486
4739
  return { ancestors: [], children: [], truncated: false };
4487
4740
  }
4488
4741
  const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
4489
4742
  if (!lineage) {
4490
- throw new HTTPException9(404, { message: "session not found" });
4743
+ throw new HTTPException10(404, { message: "session not found" });
4491
4744
  }
4492
4745
  return lineage;
4493
4746
  }
@@ -4551,7 +4804,7 @@ async function createValidatedScheduledTask(input) {
4551
4804
  async function requireScheduledTaskRig(db, workspaceId, rigId) {
4552
4805
  const rig = await getRig3(db, workspaceId, rigId);
4553
4806
  if (!rig) {
4554
- throw new HTTPException10(422, { message: `unknown rigId: ${rigId}` });
4807
+ throw new HTTPException11(422, { message: `unknown rigId: ${rigId}` });
4555
4808
  }
4556
4809
  }
4557
4810
  async function validatedScheduledTaskUpdate(input) {
@@ -4578,7 +4831,7 @@ async function validatedScheduledTaskUpdate(input) {
4578
4831
  if (input.payload.variableSetId !== void 0) {
4579
4832
  const nextVariableSetId = input.payload.variableSetId;
4580
4833
  if ((input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
4581
- throw new HTTPException10(409, {
4834
+ throw new HTTPException11(409, {
4582
4835
  message: "cannot change variableSet of a task with a live reusable session; recreate the task"
4583
4836
  });
4584
4837
  }
@@ -4608,7 +4861,7 @@ async function validatedScheduledTaskUpdate(input) {
4608
4861
  if (willHaveVariableSet) {
4609
4862
  requirePermission(input.grant, "variable-sets:use");
4610
4863
  }
4611
- update.agentConfig = await validateScheduledTaskAgentConfig({
4864
+ const nextAgentConfig = await validateScheduledTaskAgentConfig({
4612
4865
  settings: input.settings,
4613
4866
  db: input.db,
4614
4867
  objectStorage: input.objectStorage,
@@ -4617,13 +4870,19 @@ async function validatedScheduledTaskUpdate(input) {
4617
4870
  payload: { agentConfig: input.payload.agentConfig },
4618
4871
  ...input.toolsProvided !== void 0 ? { toolsProvided: input.toolsProvided } : {}
4619
4872
  });
4873
+ if (input.existing.reusableSessionId && input.existing.runMode === "reusable_session" && (input.existing.agentConfig.slackBotConnectionId ?? null) !== (nextAgentConfig.slackBotConnectionId ?? null)) {
4874
+ throw new HTTPException11(409, {
4875
+ message: "cannot change the OpenGeni Slack bot connection of a task with a live reusable session; recreate the task"
4876
+ });
4877
+ }
4878
+ update.agentConfig = nextAgentConfig;
4620
4879
  }
4621
4880
  return update;
4622
4881
  }
4623
4882
  async function requireScheduledTaskForApi(db, workspaceId, taskId) {
4624
4883
  const task = await getScheduledTask(db, workspaceId, taskId);
4625
4884
  if (!task) {
4626
- throw new HTTPException10(404, { message: "scheduled task not found" });
4885
+ throw new HTTPException11(404, { message: "scheduled task not found" });
4627
4886
  }
4628
4887
  return task;
4629
4888
  }
@@ -4688,13 +4947,26 @@ async function validateScheduledTaskAgentConfig(input) {
4688
4947
  const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
4689
4948
  const prompt = input.payload.agentConfig.prompt.trim();
4690
4949
  if (!prompt) {
4691
- throw new HTTPException10(422, { message: "scheduled task prompt is required" });
4950
+ throw new HTTPException11(422, { message: "scheduled task prompt is required" });
4951
+ }
4952
+ if (hasReservedOpenGeniSlackBotSessionMetadata(input.payload.agentConfig.metadata)) {
4953
+ throw new HTTPException11(422, {
4954
+ message: `${OPENGENI_SLACK_BOT_SESSION_METADATA_KEY3} is reserved for scheduler routing`
4955
+ });
4692
4956
  }
4693
4957
  await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
4694
4958
  if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
4695
- throw new HTTPException10(503, { message: "object storage is not configured" });
4959
+ throw new HTTPException11(503, { message: "object storage is not configured" });
4696
4960
  }
4697
4961
  await validateFileResources(input.db, input.workspaceId, resources);
4962
+ if (input.payload.agentConfig.slackBotConnectionId) {
4963
+ await validateOpenGeniSlackBotConnectionSelection(
4964
+ input.db,
4965
+ input.grant,
4966
+ input.workspaceId,
4967
+ input.payload.agentConfig.slackBotConnectionId
4968
+ );
4969
+ }
4698
4970
  const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
4699
4971
  if (requestedMaxDepth !== void 0) {
4700
4972
  const workspace = await requireWorkspace2(input.db, input.workspaceId);
@@ -4702,7 +4974,7 @@ async function validateScheduledTaskAgentConfig(input) {
4702
4974
  const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
4703
4975
  const inheritedMaxDepth = typeof workspaceMaxDepth === "number" ? workspaceMaxDepth : deploymentPolicy.maxNestedAgentDepth;
4704
4976
  if (requestedMaxDepth > inheritedMaxDepth && !hasPermission(input.grant.permissions, "workspace:admin")) {
4705
- throw new HTTPException10(403, {
4977
+ throw new HTTPException11(403, {
4706
4978
  message: `scheduled task maxNestedAgentDepth ${requestedMaxDepth} exceeds inherited limit ${inheritedMaxDepth}; workspace:admin is required to increase it`
4707
4979
  });
4708
4980
  }
@@ -4720,19 +4992,19 @@ function validateScheduledTaskSchedule(schedule) {
4720
4992
  return;
4721
4993
  }
4722
4994
  if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
4723
- throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
4995
+ throw new HTTPException11(422, { message: "interval schedule endAt must be after startAt" });
4724
4996
  }
4725
4997
  }
4726
4998
  function trimmedScheduledTaskName(name) {
4727
4999
  const trimmed = name.trim();
4728
5000
  if (!trimmed) {
4729
- throw new HTTPException10(422, { message: "scheduled task name is required" });
5001
+ throw new HTTPException11(422, { message: "scheduled task name is required" });
4730
5002
  }
4731
5003
  return trimmed;
4732
5004
  }
4733
5005
 
4734
5006
  // src/domain/workspace-members.ts
4735
- import { HTTPException as HTTPException11 } from "hono/http-exception";
5007
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
4736
5008
  var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
4737
5009
  function memberCanAdminister(member) {
4738
5010
  return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
@@ -4742,25 +5014,25 @@ function isUserMember(member) {
4742
5014
  }
4743
5015
  function resolveMemberSubjectId(userId) {
4744
5016
  if (!userId) {
4745
- throw new HTTPException11(404, { message: "user is not registered" });
5017
+ throw new HTTPException12(404, { message: "user is not registered" });
4746
5018
  }
4747
5019
  return `user:${userId}`;
4748
5020
  }
4749
5021
  function assertWorkspaceMemberRemovable(input) {
4750
5022
  const { members, subjectId, callerSubjectId } = input;
4751
5023
  if (subjectId === callerSubjectId) {
4752
- throw new HTTPException11(409, { message: "you cannot remove your own membership" });
5024
+ throw new HTTPException12(409, { message: "you cannot remove your own membership" });
4753
5025
  }
4754
5026
  const target = members.find((member) => member.subjectId === subjectId);
4755
5027
  if (!target) {
4756
- throw new HTTPException11(404, { message: "member not found" });
5028
+ throw new HTTPException12(404, { message: "member not found" });
4757
5029
  }
4758
5030
  if (memberCanAdminister(target)) {
4759
5031
  const remainingAdmins = members.filter(
4760
5032
  (member) => member.subjectId !== subjectId && memberCanAdminister(member)
4761
5033
  );
4762
5034
  if (remainingAdmins.length === 0) {
4763
- throw new HTTPException11(409, {
5035
+ throw new HTTPException12(409, {
4764
5036
  message: "cannot remove the last member who can manage this workspace"
4765
5037
  });
4766
5038
  }
@@ -4768,10 +5040,10 @@ function assertWorkspaceMemberRemovable(input) {
4768
5040
  }
4769
5041
  function assertWorkspaceDeletable(input) {
4770
5042
  if (input.workspaceCountForAccount <= 1) {
4771
- throw new HTTPException11(409, { message: "cannot delete the account's only workspace" });
5043
+ throw new HTTPException12(409, { message: "cannot delete the account's only workspace" });
4772
5044
  }
4773
5045
  if (input.activeSessionCount > 0) {
4774
- throw new HTTPException11(409, {
5046
+ throw new HTTPException12(409, {
4775
5047
  message: "stop the workspace's running sessions before deleting it"
4776
5048
  });
4777
5049
  }
@@ -4784,24 +5056,105 @@ import {
4784
5056
  } from "@opengeni/contracts";
4785
5057
  import {
4786
5058
  getNewSessionDraftInTransaction,
5059
+ getEnrollment as getEnrollment3,
5060
+ getRig as getRig4,
5061
+ getSandbox as getSandbox4,
5062
+ getVariableSet as getVariableSet4,
4787
5063
  NewSessionDraftAccessError,
5064
+ newSessionDraftToolsProvided,
5065
+ publicNewSessionDraftOptions,
5066
+ requireFile as requireFile2,
4788
5067
  saveNewSessionDraftInTransaction,
4789
5068
  withWorkspaceSubjectRls as withWorkspaceSubjectRls2
4790
5069
  } from "@opengeni/db";
4791
- import { HTTPException as HTTPException12 } from "hono/http-exception";
5070
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
5071
+ function hasOwn(value, key) {
5072
+ return typeof value === "object" && value !== null && Object.hasOwn(value, key);
5073
+ }
4792
5074
  function mapNewSessionDraft(row) {
4793
5075
  if (!row) return null;
4794
5076
  return NewSessionDraft.parse({
4795
5077
  revision: row.revision,
4796
5078
  text: row.text,
4797
5079
  resources: row.resources,
4798
- tools: row.tools,
5080
+ tools: newSessionDraftToolsProvided(row) ? row.tools : [],
5081
+ toolsProvided: newSessionDraftToolsProvided(row),
4799
5082
  model: row.model,
4800
5083
  reasoningEffort: row.reasoningEffort,
4801
- options: row.sessionOptions,
5084
+ options: publicNewSessionDraftOptions(row),
4802
5085
  updatedAt: row.updatedAt.toISOString()
4803
5086
  });
4804
5087
  }
5088
+ async function hydrateNewSessionDraft(deps, grant, workspaceId, row) {
5089
+ if (!row) return null;
5090
+ const mapped = mapNewSessionDraft(row);
5091
+ if (!mapped) return null;
5092
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
5093
+ deps.db,
5094
+ workspaceId,
5095
+ deps.settings
5096
+ );
5097
+ const resources = [];
5098
+ for (const resource of mapped.resources) {
5099
+ if (resource.kind === "repository") {
5100
+ try {
5101
+ await validateGitHubRepositorySelection(deps.db, workspaceId, [resource]);
5102
+ resources.push(resource);
5103
+ } catch (error) {
5104
+ if (isAuthoritativeGitHubRepositorySelectionError(error)) {
5105
+ continue;
5106
+ }
5107
+ resources.push(resource);
5108
+ }
5109
+ continue;
5110
+ }
5111
+ try {
5112
+ const file = await requireFile2(deps.db, workspaceId, resource.fileId);
5113
+ if (file.status === "ready") resources.push(resource);
5114
+ } catch {
5115
+ }
5116
+ }
5117
+ const options = { ...mapped.options };
5118
+ if (options.variableSetId) {
5119
+ if (!hasPermission(grant.permissions, "variable-sets:use") || !await getVariableSet4(deps.db, workspaceId, options.variableSetId)) {
5120
+ delete options.variableSetId;
5121
+ }
5122
+ }
5123
+ if (options.rigId) {
5124
+ const rig = await getRig4(deps.db, workspaceId, options.rigId);
5125
+ if (!rig?.activeVersion) delete options.rigId;
5126
+ }
5127
+ if (options.targetSandboxId) {
5128
+ const sandbox = await getSandbox4(deps.db, workspaceId, options.targetSandboxId);
5129
+ const enrollment = sandbox?.enrollmentId ? await getEnrollment3(deps.db, workspaceId, sandbox.enrollmentId) : null;
5130
+ if (!sandbox || sandbox.kind !== "selfhosted" || !enrollment || enrollment.status !== "active") {
5131
+ delete options.targetSandboxId;
5132
+ delete options.workingDir;
5133
+ delete options.sandboxBackend;
5134
+ }
5135
+ }
5136
+ let tools = [];
5137
+ if (mapped.toolsProvided) {
5138
+ try {
5139
+ tools = validateToolRefs(mapped.tools, runtimeSettings);
5140
+ } catch {
5141
+ tools = mapped.tools.filter((tool) => {
5142
+ try {
5143
+ validateToolRefs([tool], runtimeSettings);
5144
+ return true;
5145
+ } catch {
5146
+ return false;
5147
+ }
5148
+ });
5149
+ }
5150
+ }
5151
+ return {
5152
+ ...mapped,
5153
+ resources,
5154
+ tools,
5155
+ options
5156
+ };
5157
+ }
4805
5158
  async function getActorNewSessionDraft(deps, grant, workspaceId) {
4806
5159
  const row = await withWorkspaceSubjectRls2(
4807
5160
  deps.db,
@@ -4812,11 +5165,12 @@ async function getActorNewSessionDraft(deps, grant, workspaceId) {
4812
5165
  subjectId: grant.subjectId
4813
5166
  })
4814
5167
  );
4815
- return mapNewSessionDraft(row) ?? {
5168
+ return await hydrateNewSessionDraft(deps, grant, workspaceId, row) ?? {
4816
5169
  revision: 0,
4817
5170
  text: "",
4818
5171
  resources: [],
4819
5172
  tools: [],
5173
+ toolsProvided: false,
4820
5174
  model: deps.settings.openaiModel,
4821
5175
  reasoningEffort: deps.settings.openaiReasoningEffort,
4822
5176
  options: {},
@@ -4825,16 +5179,17 @@ async function getActorNewSessionDraft(deps, grant, workspaceId) {
4825
5179
  }
4826
5180
  async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
4827
5181
  const input = SaveNewSessionDraftRequest.parse(rawInput);
5182
+ const toolsProvided = hasOwn(rawInput, "toolsProvided") ? input.toolsProvided : true;
4828
5183
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
4829
5184
  deps.db,
4830
5185
  workspaceId,
4831
5186
  deps.settings
4832
5187
  );
4833
5188
  const resources = normalizeResources(input.resources);
4834
- const tools = validateToolRefs(input.tools, runtimeSettings);
5189
+ const tools = toolsProvided ? validateToolRefs(input.tools, runtimeSettings) : [];
4835
5190
  await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
4836
5191
  if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
4837
- throw new HTTPException12(503, { message: "object storage is not configured" });
5192
+ throw new HTTPException13(503, { message: "object storage is not configured" });
4838
5193
  }
4839
5194
  await validateFileResources(deps.db, workspaceId, resources);
4840
5195
  assertConfiguredModel(deps.settings, input.model);
@@ -4853,6 +5208,7 @@ async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
4853
5208
  text: input.text,
4854
5209
  resources,
4855
5210
  tools,
5211
+ toolsProvided,
4856
5212
  model: input.model,
4857
5213
  reasoningEffort: input.reasoningEffort,
4858
5214
  options: input.options,
@@ -4867,7 +5223,7 @@ async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
4867
5223
  return mapNewSessionDraft(saved);
4868
5224
  } catch (error) {
4869
5225
  if (error instanceof NewSessionDraftAccessError) {
4870
- throw new HTTPException12(403, { message: error.message });
5226
+ throw new HTTPException13(403, { message: error.message });
4871
5227
  }
4872
5228
  throw error;
4873
5229
  }
@@ -5405,7 +5761,12 @@ export {
5405
5761
  getCapabilityPack,
5406
5762
  getHumanComposerDraft,
5407
5763
  hasPermission,
5764
+ hasReservedOpenGeniSlackBotMetadata,
5765
+ hasReservedOpenGeniSlackBotSessionMetadata,
5766
+ isAuthoritativeGitHubRepositorySelectionError,
5408
5767
  isBuiltInCapabilityPack,
5768
+ isOpenGeniSlackBotConnection,
5769
+ isTrustedScheduledSlackBotSession,
5409
5770
  isUserMember,
5410
5771
  listCapabilityPacks,
5411
5772
  listFleet,
@@ -5420,6 +5781,7 @@ export {
5420
5781
  moveHumanQueuePrompt,
5421
5782
  normalizeResources,
5422
5783
  officialMcpRegistryUrl,
5784
+ openGeniSlackBotMetadata,
5423
5785
  postUserMessageTurn,
5424
5786
  promoteSetupAppendChange,
5425
5787
  promoteVerifiedDefinitionEditChangeForApi,
@@ -5436,6 +5798,7 @@ export {
5436
5798
  requireAccessGrant,
5437
5799
  requireEnvironmentEncryption,
5438
5800
  requireLimit,
5801
+ requireOpenGeniSlackBotConnection,
5439
5802
  requirePermission,
5440
5803
  requireQueuedTurnForApi,
5441
5804
  requireRigChangeForApi,
@@ -5454,11 +5817,13 @@ export {
5454
5817
  runOnSandbox,
5455
5818
  saveActorNewSessionDraft,
5456
5819
  saveHumanComposerDraft,
5820
+ scheduledSlackBotConnectionId,
5457
5821
  scheduledTaskTemporalScheduleId,
5458
5822
  scheduledTaskToolsProvided,
5459
5823
  scheduledTaskTriggerToken,
5460
5824
  sendAgentSessionMessage,
5461
5825
  sessionSpawnDenialEnvelope,
5826
+ sessionToolPolicyAllowsDefaultNativeTools,
5462
5827
  sessionWithEffectiveToolPolicy,
5463
5828
  settingsWithEnabledCapabilityMcpServers,
5464
5829
  settingsWithMcpCapabilityServers,
@@ -5472,11 +5837,13 @@ export {
5472
5837
  updateRigForApi,
5473
5838
  updateSessionMcpApprovalPolicy,
5474
5839
  updateSessionTitle,
5840
+ updateSessionToolPolicy,
5475
5841
  validateFileResources,
5476
5842
  validateGitHubRepositorySelection,
5477
5843
  validateGitHubRepositorySelectionShape,
5478
5844
  validateGitHubRepositorySelectionShapes,
5479
5845
  validateMcpCapabilityConnection,
5846
+ validateOpenGeniSlackBotConnectionSelection,
5480
5847
  validateToolRefs,
5481
5848
  validateToolRefsForSessionPolicy,
5482
5849
  validateVariableSetAttachment,