@opengeni/core 0.11.8 → 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.d.ts +35 -9
- package/dist/index.js +233 -120
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/dependencies.ts +2 -0
- package/src/domain/scheduled-tasks.ts +31 -1
- package/src/domain/sessions.ts +7 -0
- package/src/domain/slack-bot.ts +130 -0
- package/src/index.ts +1 -0
- package/src/sandbox/routing.ts +284 -243
package/dist/index.js
CHANGED
|
@@ -83,7 +83,8 @@ function routingEnabled(settings) {
|
|
|
83
83
|
}
|
|
84
84
|
function wrapChannelABoxWithRouting(services, ids, established) {
|
|
85
85
|
const { db, settings, bus } = services;
|
|
86
|
-
const
|
|
86
|
+
const homeLease = ids.homeLease;
|
|
87
|
+
const beforeMutation = homeLease ? async ({
|
|
87
88
|
op,
|
|
88
89
|
backend
|
|
89
90
|
}) => {
|
|
@@ -99,15 +100,15 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
99
100
|
sessionId: ids.sessionId,
|
|
100
101
|
requestId: ids.directRequest.requestId,
|
|
101
102
|
holderId: ids.directRequest.holderId,
|
|
102
|
-
sandboxGroupId:
|
|
103
|
+
sandboxGroupId: homeLease.sandboxGroupId,
|
|
103
104
|
expectedEpoch: backend.leaseEpoch,
|
|
104
105
|
expectedInstanceId: backend.providerInstanceId,
|
|
105
106
|
routeTargetId: backend.sandboxId,
|
|
106
107
|
routeEpoch: backend.activeEpoch,
|
|
107
108
|
operation: op
|
|
108
109
|
});
|
|
109
|
-
};
|
|
110
|
-
const afterMutation = async ({
|
|
110
|
+
} : void 0;
|
|
111
|
+
const afterMutation = homeLease ? async ({
|
|
111
112
|
op,
|
|
112
113
|
backend,
|
|
113
114
|
admission,
|
|
@@ -133,7 +134,7 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
133
134
|
kind: "direct",
|
|
134
135
|
requestId: ids.directRequest.requestId,
|
|
135
136
|
holderId: ids.directRequest.holderId,
|
|
136
|
-
sandboxGroupId:
|
|
137
|
+
sandboxGroupId: homeLease.sandboxGroupId,
|
|
137
138
|
expectedEpoch: backend.leaseEpoch,
|
|
138
139
|
expectedInstanceId: backend.providerInstanceId,
|
|
139
140
|
routeTargetId: exactAdmission.routeTargetId,
|
|
@@ -148,7 +149,7 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
148
149
|
sessionId: ids.sessionId,
|
|
149
150
|
requestId: ids.directRequest.requestId,
|
|
150
151
|
holderId: ids.directRequest.holderId,
|
|
151
|
-
sandboxGroupId:
|
|
152
|
+
sandboxGroupId: homeLease.sandboxGroupId,
|
|
152
153
|
expectedEpoch: backend.leaseEpoch,
|
|
153
154
|
expectedInstanceId: backend.providerInstanceId,
|
|
154
155
|
routeTargetId: exactAdmission.routeTargetId,
|
|
@@ -157,8 +158,8 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
157
158
|
operation: op,
|
|
158
159
|
outcome
|
|
159
160
|
});
|
|
160
|
-
};
|
|
161
|
-
const beforeProcessMutation = async ({
|
|
161
|
+
} : void 0;
|
|
162
|
+
const beforeProcessMutation = homeLease ? async ({
|
|
162
163
|
op,
|
|
163
164
|
process
|
|
164
165
|
}) => await advanceWorkspaceGenerationForRetainedProcess(db, {
|
|
@@ -167,8 +168,8 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
167
168
|
sessionId: ids.sessionId,
|
|
168
169
|
processId: process.id,
|
|
169
170
|
operation: op
|
|
170
|
-
});
|
|
171
|
-
const afterProcessMutation = async ({
|
|
171
|
+
}) : void 0;
|
|
172
|
+
const afterProcessMutation = homeLease ? async ({
|
|
172
173
|
op,
|
|
173
174
|
process,
|
|
174
175
|
admission,
|
|
@@ -186,8 +187,8 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
186
187
|
operation: op,
|
|
187
188
|
outcome
|
|
188
189
|
});
|
|
189
|
-
};
|
|
190
|
-
const settleProcess = async ({
|
|
190
|
+
} : void 0;
|
|
191
|
+
const settleProcess = homeLease ? async ({
|
|
191
192
|
backend,
|
|
192
193
|
process,
|
|
193
194
|
proof
|
|
@@ -201,7 +202,9 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
201
202
|
processId: process.id
|
|
202
203
|
});
|
|
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) {
|
|
204
|
-
throw new Error(
|
|
205
|
+
throw new Error(
|
|
206
|
+
"API retained-process settlement lost its exact durable backend identity"
|
|
207
|
+
);
|
|
205
208
|
}
|
|
206
209
|
await settleRetainedProcess(db, {
|
|
207
210
|
accountId: ids.accountId,
|
|
@@ -214,7 +217,7 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
214
217
|
reason: proof.reason,
|
|
215
218
|
idleGraceMs: settings.sandboxIdleGraceMs
|
|
216
219
|
});
|
|
217
|
-
};
|
|
220
|
+
} : void 0;
|
|
218
221
|
const resolver = makeActiveBackendResolver({
|
|
219
222
|
workspaceId: ids.workspaceId,
|
|
220
223
|
defaultBackend: established.session,
|
|
@@ -230,21 +233,34 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
230
233
|
},
|
|
231
234
|
controlRpcFactory: controlRpcFactory(bus),
|
|
232
235
|
relay: relayConfigFromSettings(settings),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
} : {}
|
|
240
254
|
});
|
|
241
255
|
const proxy = new RoutingSandboxSession({
|
|
242
256
|
defaultResolved: {
|
|
243
257
|
session: established.session,
|
|
244
258
|
sandboxId: null,
|
|
245
259
|
kind: established.backendId,
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
...homeLease ? {
|
|
261
|
+
leaseEpoch: homeLease.leaseEpoch,
|
|
262
|
+
providerInstanceId: homeLease.instanceId
|
|
263
|
+
} : {}
|
|
248
264
|
},
|
|
249
265
|
readPointer: async () => {
|
|
250
266
|
if (!routingEnabled(settings)) {
|
|
@@ -254,36 +270,38 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
254
270
|
return pointer ?? { activeSandboxId: null, activeEpoch: 0 };
|
|
255
271
|
},
|
|
256
272
|
resolveActiveBackend: resolver,
|
|
257
|
-
beforeMutation,
|
|
258
|
-
afterMutation,
|
|
259
|
-
beforeProcessMutation,
|
|
260
|
-
afterProcessMutation,
|
|
261
|
-
settleProcess,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
};
|
|
279
303
|
}
|
|
280
|
-
|
|
281
|
-
const restore = lease?.recovery.restore.status;
|
|
282
|
-
return {
|
|
283
|
-
leaseEpoch: lease?.leaseEpoch ?? ids.homeLease.leaseEpoch,
|
|
284
|
-
recovery: marked.status === "stale" ? "superseded" : restore === "pending" ? "pending" : restore === "degraded" ? "degraded" : "unrecoverable"
|
|
285
|
-
};
|
|
286
|
-
}
|
|
304
|
+
} : {}
|
|
287
305
|
});
|
|
288
306
|
return { ...established, session: proxy };
|
|
289
307
|
}
|
|
@@ -3291,6 +3309,7 @@ function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDe
|
|
|
3291
3309
|
}
|
|
3292
3310
|
|
|
3293
3311
|
// src/domain/scheduled-tasks.ts
|
|
3312
|
+
import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY as OPENGENI_SLACK_BOT_SESSION_METADATA_KEY3 } from "@opengeni/contracts";
|
|
3294
3313
|
import {
|
|
3295
3314
|
createScheduledTask,
|
|
3296
3315
|
deleteScheduledTask,
|
|
@@ -3300,7 +3319,7 @@ import {
|
|
|
3300
3319
|
requireWorkspace as requireWorkspace2,
|
|
3301
3320
|
updateScheduledTask
|
|
3302
3321
|
} from "@opengeni/db";
|
|
3303
|
-
import { HTTPException as
|
|
3322
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
3304
3323
|
|
|
3305
3324
|
// src/domain/sessions.ts
|
|
3306
3325
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
@@ -3313,6 +3332,7 @@ import {
|
|
|
3313
3332
|
import {
|
|
3314
3333
|
CreateSessionRequest,
|
|
3315
3334
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
3335
|
+
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY as OPENGENI_SLACK_BOT_SESSION_METADATA_KEY2,
|
|
3316
3336
|
SessionSpawnDenial,
|
|
3317
3337
|
ServiceTurnInitiator,
|
|
3318
3338
|
ServiceTurnInitiatorContext,
|
|
@@ -3359,7 +3379,68 @@ import {
|
|
|
3359
3379
|
publishDurableSessionEvents,
|
|
3360
3380
|
publishDurableWorkspaceControlEvent
|
|
3361
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";
|
|
3362
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
|
|
3363
3444
|
var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
|
|
3364
3445
|
var maxSessionMcpCredentialHeaders = 16;
|
|
3365
3446
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
@@ -3391,7 +3472,7 @@ function sessionSpawnDenialEnvelope(error) {
|
|
|
3391
3472
|
function serviceInitiatorForGrant(grant) {
|
|
3392
3473
|
if (!grant.serviceInitiator) {
|
|
3393
3474
|
if (grant.serviceInitiatorContext) {
|
|
3394
|
-
throw new
|
|
3475
|
+
throw new HTTPException10(403, {
|
|
3395
3476
|
message: "service initiator context requires a signed service initiator"
|
|
3396
3477
|
});
|
|
3397
3478
|
}
|
|
@@ -3399,13 +3480,13 @@ function serviceInitiatorForGrant(grant) {
|
|
|
3399
3480
|
}
|
|
3400
3481
|
const initiator = ServiceTurnInitiator.safeParse(grant.serviceInitiator);
|
|
3401
3482
|
if (!initiator.success) {
|
|
3402
|
-
throw new
|
|
3483
|
+
throw new HTTPException10(403, {
|
|
3403
3484
|
message: "a delegated command initiator must be a bounded service principal"
|
|
3404
3485
|
});
|
|
3405
3486
|
}
|
|
3406
3487
|
const context = ServiceTurnInitiatorContext.safeParse(grant.serviceInitiatorContext ?? {});
|
|
3407
3488
|
if (!context.success) {
|
|
3408
|
-
throw new
|
|
3489
|
+
throw new HTTPException10(403, {
|
|
3409
3490
|
message: "delegated service initiator context is invalid or reserved"
|
|
3410
3491
|
});
|
|
3411
3492
|
}
|
|
@@ -3413,7 +3494,7 @@ function serviceInitiatorForGrant(grant) {
|
|
|
3413
3494
|
const callerAttemptId = grant.metadata?.["attemptId"];
|
|
3414
3495
|
const callerExecutionGeneration = grant.metadata?.["executionGeneration"];
|
|
3415
3496
|
if (callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0) {
|
|
3416
|
-
throw new
|
|
3497
|
+
throw new HTTPException10(403, {
|
|
3417
3498
|
message: "a service initiator cannot replace an exact agent-attempt initiator"
|
|
3418
3499
|
});
|
|
3419
3500
|
}
|
|
@@ -3431,7 +3512,7 @@ function creationInitiatorForGrant(grant) {
|
|
|
3431
3512
|
const hasCallerTurnClaim = callerTurnId !== void 0 || callerAttemptId !== void 0 || callerExecutionGeneration !== void 0;
|
|
3432
3513
|
if (hasCallerTurnClaim) {
|
|
3433
3514
|
if (typeof callerSessionId !== "string" || typeof callerTurnId !== "string" || typeof callerAttemptId !== "string" || typeof callerExecutionGeneration !== "number" || !Number.isSafeInteger(callerExecutionGeneration) || callerExecutionGeneration < 1) {
|
|
3434
|
-
throw new
|
|
3515
|
+
throw new HTTPException10(403, { message: "caller attempt claims are incomplete" });
|
|
3435
3516
|
}
|
|
3436
3517
|
const actor = {
|
|
3437
3518
|
type: "agent_attempt",
|
|
@@ -3460,27 +3541,27 @@ function normalizedSessionMcpCredentialHeaders(headers) {
|
|
|
3460
3541
|
}
|
|
3461
3542
|
const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
|
|
3462
3543
|
if (entries.length > maxSessionMcpCredentialHeaders) {
|
|
3463
|
-
throw new
|
|
3544
|
+
throw new HTTPException10(422, {
|
|
3464
3545
|
message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
|
|
3465
3546
|
});
|
|
3466
3547
|
}
|
|
3467
3548
|
const seen = /* @__PURE__ */ new Set();
|
|
3468
3549
|
for (const [name, value] of entries) {
|
|
3469
3550
|
if (!sessionMcpCredentialHeaderName.test(name)) {
|
|
3470
|
-
throw new
|
|
3551
|
+
throw new HTTPException10(422, { message: `invalid credential header name: ${name}` });
|
|
3471
3552
|
}
|
|
3472
3553
|
const lower = name.toLowerCase();
|
|
3473
3554
|
if (seen.has(lower)) {
|
|
3474
|
-
throw new
|
|
3555
|
+
throw new HTTPException10(422, { message: `duplicate credential header name: ${name}` });
|
|
3475
3556
|
}
|
|
3476
3557
|
seen.add(lower);
|
|
3477
3558
|
if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
|
|
3478
|
-
throw new
|
|
3559
|
+
throw new HTTPException10(422, {
|
|
3479
3560
|
message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
|
|
3480
3561
|
});
|
|
3481
3562
|
}
|
|
3482
3563
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
3483
|
-
throw new
|
|
3564
|
+
throw new HTTPException10(422, {
|
|
3484
3565
|
message: `credential header ${name} contains forbidden control characters`
|
|
3485
3566
|
});
|
|
3486
3567
|
}
|
|
@@ -3547,11 +3628,11 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
3547
3628
|
const metadata = [];
|
|
3548
3629
|
for (const server of servers) {
|
|
3549
3630
|
if (seenIds.has(server.id)) {
|
|
3550
|
-
throw new
|
|
3631
|
+
throw new HTTPException10(422, { message: `duplicate session MCP server id: ${server.id}` });
|
|
3551
3632
|
}
|
|
3552
3633
|
seenIds.add(server.id);
|
|
3553
3634
|
if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
|
|
3554
|
-
throw new
|
|
3635
|
+
throw new HTTPException10(422, { message: `MCP server id already exists: ${server.id}` });
|
|
3555
3636
|
}
|
|
3556
3637
|
const headers = normalizedSessionMcpCredentialHeaders(server.headers);
|
|
3557
3638
|
const headersEncrypted = Object.fromEntries(
|
|
@@ -3591,13 +3672,13 @@ function validateInheritedSessionMcpServersForCreate(servers) {
|
|
|
3591
3672
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3592
3673
|
for (const server of servers) {
|
|
3593
3674
|
if (seenIds.has(server.id)) {
|
|
3594
|
-
throw new
|
|
3675
|
+
throw new HTTPException10(422, {
|
|
3595
3676
|
message: `duplicate inherited session MCP server id: ${server.id}`
|
|
3596
3677
|
});
|
|
3597
3678
|
}
|
|
3598
3679
|
seenIds.add(server.id);
|
|
3599
3680
|
if (reservedSessionMcpServerIds.has(server.id)) {
|
|
3600
|
-
throw new
|
|
3681
|
+
throw new HTTPException10(422, {
|
|
3601
3682
|
message: `reserved inherited session MCP server id: ${server.id}`
|
|
3602
3683
|
});
|
|
3603
3684
|
}
|
|
@@ -3629,13 +3710,13 @@ function validateSessionMcpCredentialUpdates(input) {
|
|
|
3629
3710
|
const seenIds = /* @__PURE__ */ new Set();
|
|
3630
3711
|
const encryptedUpdates = input.updates.map((update) => {
|
|
3631
3712
|
if (seenIds.has(update.id)) {
|
|
3632
|
-
throw new
|
|
3713
|
+
throw new HTTPException10(422, {
|
|
3633
3714
|
message: `duplicate session MCP credential update id: ${update.id}`
|
|
3634
3715
|
});
|
|
3635
3716
|
}
|
|
3636
3717
|
seenIds.add(update.id);
|
|
3637
3718
|
if (!knownIds.has(update.id)) {
|
|
3638
|
-
throw new
|
|
3719
|
+
throw new HTTPException10(422, { message: `unknown session MCP server id: ${update.id}` });
|
|
3639
3720
|
}
|
|
3640
3721
|
const headers = normalizedSessionMcpCredentialHeaders(update.headers);
|
|
3641
3722
|
return {
|
|
@@ -3739,7 +3820,7 @@ async function createAndStartSession(input) {
|
|
|
3739
3820
|
async function finishStartSession(input, session) {
|
|
3740
3821
|
if (input.seedTargetSandbox) {
|
|
3741
3822
|
if (session.sandboxBackend === "none") {
|
|
3742
|
-
throw new
|
|
3823
|
+
throw new HTTPException10(422, {
|
|
3743
3824
|
message: "cannot target a machine for a session with no sandbox (backend: none)"
|
|
3744
3825
|
});
|
|
3745
3826
|
}
|
|
@@ -3759,7 +3840,7 @@ async function finishStartSession(input, session) {
|
|
|
3759
3840
|
input.seedTargetSandbox.workingDir ?? null
|
|
3760
3841
|
);
|
|
3761
3842
|
if (!seeded.swapped) {
|
|
3762
|
-
throw new
|
|
3843
|
+
throw new HTTPException10(422, {
|
|
3763
3844
|
message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
|
|
3764
3845
|
});
|
|
3765
3846
|
}
|
|
@@ -3811,7 +3892,7 @@ function canonicalConfiguredModel(settings, model) {
|
|
|
3811
3892
|
if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
3812
3893
|
return canonicalModel;
|
|
3813
3894
|
}
|
|
3814
|
-
throw new
|
|
3895
|
+
throw new HTTPException10(422, { message: `model is not available: ${model}` });
|
|
3815
3896
|
}
|
|
3816
3897
|
function assertConfiguredModel(settings, model) {
|
|
3817
3898
|
canonicalConfiguredModel(settings, model);
|
|
@@ -3834,7 +3915,7 @@ async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model
|
|
|
3834
3915
|
modelId: canonicalModel
|
|
3835
3916
|
});
|
|
3836
3917
|
if (!verdict.allowed) {
|
|
3837
|
-
throw new
|
|
3918
|
+
throw new HTTPException10(422, {
|
|
3838
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`
|
|
3839
3920
|
});
|
|
3840
3921
|
}
|
|
@@ -3842,10 +3923,10 @@ async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model
|
|
|
3842
3923
|
async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
|
|
3843
3924
|
const turn = await getSessionTurn(db, workspaceId, turnId);
|
|
3844
3925
|
if (!turn || turn.sessionId !== sessionId) {
|
|
3845
|
-
throw new
|
|
3926
|
+
throw new HTTPException10(404, { message: "session turn not found" });
|
|
3846
3927
|
}
|
|
3847
3928
|
if (turn.status !== "queued") {
|
|
3848
|
-
throw new
|
|
3929
|
+
throw new HTTPException10(409, {
|
|
3849
3930
|
message: `turn is ${turn.status}; only queued turns can be changed`
|
|
3850
3931
|
});
|
|
3851
3932
|
}
|
|
@@ -3898,13 +3979,13 @@ async function postUserMessageTurn(input) {
|
|
|
3898
3979
|
);
|
|
3899
3980
|
} catch (error) {
|
|
3900
3981
|
if (error instanceof QueueCommandConflictError || error instanceof SessionControlConflictError) {
|
|
3901
|
-
throw new
|
|
3982
|
+
throw new HTTPException10(409, { message: error.message });
|
|
3902
3983
|
}
|
|
3903
3984
|
if (error instanceof Error && error.message.includes("cancelled")) {
|
|
3904
|
-
throw new
|
|
3985
|
+
throw new HTTPException10(409, { message: error.message });
|
|
3905
3986
|
}
|
|
3906
3987
|
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
3907
|
-
throw new
|
|
3988
|
+
throw new HTTPException10(422, { message: error.message });
|
|
3908
3989
|
}
|
|
3909
3990
|
throw error;
|
|
3910
3991
|
}
|
|
@@ -3957,6 +4038,11 @@ async function postUserMessageTurn(input) {
|
|
|
3957
4038
|
async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
3958
4039
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
3959
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
|
+
}
|
|
3960
4046
|
if (payload.idempotencyKey) {
|
|
3961
4047
|
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
3962
4048
|
db,
|
|
@@ -3977,7 +4063,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3977
4063
|
}
|
|
3978
4064
|
const parentSession = parentSessionId ? await getSession2(db, workspaceId, parentSessionId) : null;
|
|
3979
4065
|
if (parentSessionId && !parentSession) {
|
|
3980
|
-
throw new
|
|
4066
|
+
throw new HTTPException10(404, {
|
|
3981
4067
|
message: `parent session not found in workspace: ${parentSessionId}`
|
|
3982
4068
|
});
|
|
3983
4069
|
}
|
|
@@ -4042,7 +4128,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4042
4128
|
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
4043
4129
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
4044
4130
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
4045
|
-
throw new
|
|
4131
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
4046
4132
|
}
|
|
4047
4133
|
await validateFileResources(db, workspaceId, resources);
|
|
4048
4134
|
const variableSet = payload.variableSetId ? await validateVariableSetAttachment(
|
|
@@ -4058,7 +4144,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4058
4144
|
const rig = await getRig2(db, workspaceId, requestedRigId);
|
|
4059
4145
|
if (!rig || !rig.activeVersion) {
|
|
4060
4146
|
if (payload.rigId) {
|
|
4061
|
-
throw new
|
|
4147
|
+
throw new HTTPException10(422, {
|
|
4062
4148
|
message: rig ? `rig ${payload.rigId} has no active version to bind` : `unknown rigId: ${payload.rigId}`
|
|
4063
4149
|
});
|
|
4064
4150
|
}
|
|
@@ -4084,7 +4170,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4084
4170
|
if (parentFirstPartyMcpPermissions && payload.firstPartyMcpPermissions?.some(
|
|
4085
4171
|
(permission) => !hasPermission(parentFirstPartyMcpPermissions, permission)
|
|
4086
4172
|
)) {
|
|
4087
|
-
throw new
|
|
4173
|
+
throw new HTTPException10(403, {
|
|
4088
4174
|
message: "child first-party MCP permissions may only narrow the parent session grant"
|
|
4089
4175
|
});
|
|
4090
4176
|
}
|
|
@@ -4092,19 +4178,19 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4092
4178
|
(permission) => hasPermission(grant.permissions, permission)
|
|
4093
4179
|
) : null);
|
|
4094
4180
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
4095
|
-
throw new
|
|
4181
|
+
throw new HTTPException10(422, {
|
|
4096
4182
|
message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
|
|
4097
4183
|
});
|
|
4098
4184
|
}
|
|
4099
4185
|
for (const permission of firstPartyMcpPermissions ?? []) {
|
|
4100
4186
|
if (!hasPermission(grant.permissions, permission)) {
|
|
4101
|
-
throw new
|
|
4187
|
+
throw new HTTPException10(403, {
|
|
4102
4188
|
message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
|
|
4103
4189
|
});
|
|
4104
4190
|
}
|
|
4105
4191
|
}
|
|
4106
4192
|
if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
|
|
4107
|
-
throw new
|
|
4193
|
+
throw new HTTPException10(422, {
|
|
4108
4194
|
message: "goal-bearing sessions require goals:manage in the resulting first-party MCP permission set"
|
|
4109
4195
|
});
|
|
4110
4196
|
}
|
|
@@ -4116,7 +4202,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4116
4202
|
const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
|
|
4117
4203
|
if (sandboxChoice === "shared") {
|
|
4118
4204
|
if (!parentSessionId) {
|
|
4119
|
-
throw new
|
|
4205
|
+
throw new HTTPException10(422, {
|
|
4120
4206
|
message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
|
|
4121
4207
|
});
|
|
4122
4208
|
}
|
|
@@ -4139,7 +4225,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4139
4225
|
}
|
|
4140
4226
|
if (variableSetMismatch || rigMismatch) {
|
|
4141
4227
|
if (payload.sandbox === "shared") {
|
|
4142
|
-
throw new
|
|
4228
|
+
throw new HTTPException10(422, {
|
|
4143
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."
|
|
4144
4230
|
});
|
|
4145
4231
|
}
|
|
@@ -4150,7 +4236,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4150
4236
|
} else if (typeof sandboxChoice === "object") {
|
|
4151
4237
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
4152
4238
|
if (!member) {
|
|
4153
|
-
throw new
|
|
4239
|
+
throw new HTTPException10(404, {
|
|
4154
4240
|
message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
|
|
4155
4241
|
});
|
|
4156
4242
|
}
|
|
@@ -4163,7 +4249,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4163
4249
|
if (!memberVariableSetIds.every(
|
|
4164
4250
|
(memberVariableSetId) => variableSetMatchesGroup(memberVariableSetId)
|
|
4165
4251
|
)) {
|
|
4166
|
-
throw new
|
|
4252
|
+
throw new HTTPException10(422, {
|
|
4167
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.`
|
|
4168
4254
|
});
|
|
4169
4255
|
}
|
|
@@ -4175,7 +4261,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4175
4261
|
if (!memberRigVersionIds.every(
|
|
4176
4262
|
(memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
|
|
4177
4263
|
)) {
|
|
4178
|
-
throw new
|
|
4264
|
+
throw new HTTPException10(422, {
|
|
4179
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.`
|
|
4180
4266
|
});
|
|
4181
4267
|
}
|
|
@@ -4184,7 +4270,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4184
4270
|
inheritedBackend = member.sandboxBackend;
|
|
4185
4271
|
}
|
|
4186
4272
|
if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
|
|
4187
|
-
throw new
|
|
4273
|
+
throw new HTTPException10(422, {
|
|
4188
4274
|
message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
|
|
4189
4275
|
});
|
|
4190
4276
|
}
|
|
@@ -4272,10 +4358,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4272
4358
|
});
|
|
4273
4359
|
} catch (error) {
|
|
4274
4360
|
if (error instanceof AgentCommandAuthorityError) {
|
|
4275
|
-
throw new
|
|
4361
|
+
throw new HTTPException10(403, { message: error.message });
|
|
4276
4362
|
}
|
|
4277
4363
|
if (error instanceof SessionIdConflictError) {
|
|
4278
|
-
throw new
|
|
4364
|
+
throw new HTTPException10(409, {
|
|
4279
4365
|
message: "requested session id is already in use"
|
|
4280
4366
|
});
|
|
4281
4367
|
}
|
|
@@ -4300,7 +4386,7 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
4300
4386
|
}
|
|
4301
4387
|
async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
|
|
4302
4388
|
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
4303
|
-
throw new
|
|
4389
|
+
throw new HTTPException10(503, {
|
|
4304
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"
|
|
4305
4391
|
});
|
|
4306
4392
|
}
|
|
@@ -4362,7 +4448,7 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
4362
4448
|
model: effectiveModel
|
|
4363
4449
|
});
|
|
4364
4450
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
4365
|
-
throw new
|
|
4451
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
4366
4452
|
}
|
|
4367
4453
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
4368
4454
|
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
@@ -4469,7 +4555,7 @@ async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId,
|
|
|
4469
4555
|
async (_session, context) => {
|
|
4470
4556
|
const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
|
|
4471
4557
|
if (!result.server) {
|
|
4472
|
-
throw new
|
|
4558
|
+
throw new HTTPException10(404, { message: "session MCP server not found" });
|
|
4473
4559
|
}
|
|
4474
4560
|
outcome.server = result.server;
|
|
4475
4561
|
return {
|
|
@@ -4543,7 +4629,7 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
4543
4629
|
(tool) => !validatedIds.has(`${tool.kind}:${tool.id}`)
|
|
4544
4630
|
);
|
|
4545
4631
|
if (unknown) {
|
|
4546
|
-
throw new
|
|
4632
|
+
throw new HTTPException10(422, { message: `unknown MCP server id: ${unknown.id}` });
|
|
4547
4633
|
}
|
|
4548
4634
|
return withFirstPartyTools(validatedTools, runtimeSettings);
|
|
4549
4635
|
})() : null;
|
|
@@ -4565,7 +4651,7 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
4565
4651
|
if (session.parentSessionId) {
|
|
4566
4652
|
const parent = await context.getLockedSession(session.parentSessionId);
|
|
4567
4653
|
if (!parent) {
|
|
4568
|
-
throw new
|
|
4654
|
+
throw new HTTPException10(409, { message: "parent session is no longer available" });
|
|
4569
4655
|
}
|
|
4570
4656
|
const parentTracksWorkspaceDefaults = parent.toolPolicy?.mode === "workspace_default";
|
|
4571
4657
|
const parentEffective = withFirstPartyTools(
|
|
@@ -4578,7 +4664,7 @@ async function updateSessionToolPolicy(deps, grant, sessionId, request) {
|
|
|
4578
4664
|
);
|
|
4579
4665
|
if (requestedMode === "workspace_default") {
|
|
4580
4666
|
if (!parentTracksWorkspaceDefaults) {
|
|
4581
|
-
throw new
|
|
4667
|
+
throw new HTTPException10(403, {
|
|
4582
4668
|
message: "a child may adopt workspace defaults only while its parent tracks workspace defaults"
|
|
4583
4669
|
});
|
|
4584
4670
|
}
|
|
@@ -4648,13 +4734,13 @@ async function readSessionLineage(deps, grant, sessionId) {
|
|
|
4648
4734
|
if (authorization?.relatedSessionAccess === "target") {
|
|
4649
4735
|
const session = await getSession2(deps.db, grant.workspaceId, sessionId);
|
|
4650
4736
|
if (!session) {
|
|
4651
|
-
throw new
|
|
4737
|
+
throw new HTTPException10(404, { message: "session not found" });
|
|
4652
4738
|
}
|
|
4653
4739
|
return { ancestors: [], children: [], truncated: false };
|
|
4654
4740
|
}
|
|
4655
4741
|
const lineage = await getSessionLineage(deps.db, grant.workspaceId, sessionId);
|
|
4656
4742
|
if (!lineage) {
|
|
4657
|
-
throw new
|
|
4743
|
+
throw new HTTPException10(404, { message: "session not found" });
|
|
4658
4744
|
}
|
|
4659
4745
|
return lineage;
|
|
4660
4746
|
}
|
|
@@ -4718,7 +4804,7 @@ async function createValidatedScheduledTask(input) {
|
|
|
4718
4804
|
async function requireScheduledTaskRig(db, workspaceId, rigId) {
|
|
4719
4805
|
const rig = await getRig3(db, workspaceId, rigId);
|
|
4720
4806
|
if (!rig) {
|
|
4721
|
-
throw new
|
|
4807
|
+
throw new HTTPException11(422, { message: `unknown rigId: ${rigId}` });
|
|
4722
4808
|
}
|
|
4723
4809
|
}
|
|
4724
4810
|
async function validatedScheduledTaskUpdate(input) {
|
|
@@ -4745,7 +4831,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
4745
4831
|
if (input.payload.variableSetId !== void 0) {
|
|
4746
4832
|
const nextVariableSetId = input.payload.variableSetId;
|
|
4747
4833
|
if ((input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
|
|
4748
|
-
throw new
|
|
4834
|
+
throw new HTTPException11(409, {
|
|
4749
4835
|
message: "cannot change variableSet of a task with a live reusable session; recreate the task"
|
|
4750
4836
|
});
|
|
4751
4837
|
}
|
|
@@ -4775,7 +4861,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
4775
4861
|
if (willHaveVariableSet) {
|
|
4776
4862
|
requirePermission(input.grant, "variable-sets:use");
|
|
4777
4863
|
}
|
|
4778
|
-
|
|
4864
|
+
const nextAgentConfig = await validateScheduledTaskAgentConfig({
|
|
4779
4865
|
settings: input.settings,
|
|
4780
4866
|
db: input.db,
|
|
4781
4867
|
objectStorage: input.objectStorage,
|
|
@@ -4784,13 +4870,19 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
4784
4870
|
payload: { agentConfig: input.payload.agentConfig },
|
|
4785
4871
|
...input.toolsProvided !== void 0 ? { toolsProvided: input.toolsProvided } : {}
|
|
4786
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;
|
|
4787
4879
|
}
|
|
4788
4880
|
return update;
|
|
4789
4881
|
}
|
|
4790
4882
|
async function requireScheduledTaskForApi(db, workspaceId, taskId) {
|
|
4791
4883
|
const task = await getScheduledTask(db, workspaceId, taskId);
|
|
4792
4884
|
if (!task) {
|
|
4793
|
-
throw new
|
|
4885
|
+
throw new HTTPException11(404, { message: "scheduled task not found" });
|
|
4794
4886
|
}
|
|
4795
4887
|
return task;
|
|
4796
4888
|
}
|
|
@@ -4855,13 +4947,26 @@ async function validateScheduledTaskAgentConfig(input) {
|
|
|
4855
4947
|
const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
|
|
4856
4948
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
4857
4949
|
if (!prompt) {
|
|
4858
|
-
throw new
|
|
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
|
+
});
|
|
4859
4956
|
}
|
|
4860
4957
|
await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
|
|
4861
4958
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
4862
|
-
throw new
|
|
4959
|
+
throw new HTTPException11(503, { message: "object storage is not configured" });
|
|
4863
4960
|
}
|
|
4864
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
|
+
}
|
|
4865
4970
|
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
4866
4971
|
if (requestedMaxDepth !== void 0) {
|
|
4867
4972
|
const workspace = await requireWorkspace2(input.db, input.workspaceId);
|
|
@@ -4869,7 +4974,7 @@ async function validateScheduledTaskAgentConfig(input) {
|
|
|
4869
4974
|
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
4870
4975
|
const inheritedMaxDepth = typeof workspaceMaxDepth === "number" ? workspaceMaxDepth : deploymentPolicy.maxNestedAgentDepth;
|
|
4871
4976
|
if (requestedMaxDepth > inheritedMaxDepth && !hasPermission(input.grant.permissions, "workspace:admin")) {
|
|
4872
|
-
throw new
|
|
4977
|
+
throw new HTTPException11(403, {
|
|
4873
4978
|
message: `scheduled task maxNestedAgentDepth ${requestedMaxDepth} exceeds inherited limit ${inheritedMaxDepth}; workspace:admin is required to increase it`
|
|
4874
4979
|
});
|
|
4875
4980
|
}
|
|
@@ -4887,19 +4992,19 @@ function validateScheduledTaskSchedule(schedule) {
|
|
|
4887
4992
|
return;
|
|
4888
4993
|
}
|
|
4889
4994
|
if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
|
|
4890
|
-
throw new
|
|
4995
|
+
throw new HTTPException11(422, { message: "interval schedule endAt must be after startAt" });
|
|
4891
4996
|
}
|
|
4892
4997
|
}
|
|
4893
4998
|
function trimmedScheduledTaskName(name) {
|
|
4894
4999
|
const trimmed = name.trim();
|
|
4895
5000
|
if (!trimmed) {
|
|
4896
|
-
throw new
|
|
5001
|
+
throw new HTTPException11(422, { message: "scheduled task name is required" });
|
|
4897
5002
|
}
|
|
4898
5003
|
return trimmed;
|
|
4899
5004
|
}
|
|
4900
5005
|
|
|
4901
5006
|
// src/domain/workspace-members.ts
|
|
4902
|
-
import { HTTPException as
|
|
5007
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
4903
5008
|
var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
|
|
4904
5009
|
function memberCanAdminister(member) {
|
|
4905
5010
|
return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
|
|
@@ -4909,25 +5014,25 @@ function isUserMember(member) {
|
|
|
4909
5014
|
}
|
|
4910
5015
|
function resolveMemberSubjectId(userId) {
|
|
4911
5016
|
if (!userId) {
|
|
4912
|
-
throw new
|
|
5017
|
+
throw new HTTPException12(404, { message: "user is not registered" });
|
|
4913
5018
|
}
|
|
4914
5019
|
return `user:${userId}`;
|
|
4915
5020
|
}
|
|
4916
5021
|
function assertWorkspaceMemberRemovable(input) {
|
|
4917
5022
|
const { members, subjectId, callerSubjectId } = input;
|
|
4918
5023
|
if (subjectId === callerSubjectId) {
|
|
4919
|
-
throw new
|
|
5024
|
+
throw new HTTPException12(409, { message: "you cannot remove your own membership" });
|
|
4920
5025
|
}
|
|
4921
5026
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
4922
5027
|
if (!target) {
|
|
4923
|
-
throw new
|
|
5028
|
+
throw new HTTPException12(404, { message: "member not found" });
|
|
4924
5029
|
}
|
|
4925
5030
|
if (memberCanAdminister(target)) {
|
|
4926
5031
|
const remainingAdmins = members.filter(
|
|
4927
5032
|
(member) => member.subjectId !== subjectId && memberCanAdminister(member)
|
|
4928
5033
|
);
|
|
4929
5034
|
if (remainingAdmins.length === 0) {
|
|
4930
|
-
throw new
|
|
5035
|
+
throw new HTTPException12(409, {
|
|
4931
5036
|
message: "cannot remove the last member who can manage this workspace"
|
|
4932
5037
|
});
|
|
4933
5038
|
}
|
|
@@ -4935,10 +5040,10 @@ function assertWorkspaceMemberRemovable(input) {
|
|
|
4935
5040
|
}
|
|
4936
5041
|
function assertWorkspaceDeletable(input) {
|
|
4937
5042
|
if (input.workspaceCountForAccount <= 1) {
|
|
4938
|
-
throw new
|
|
5043
|
+
throw new HTTPException12(409, { message: "cannot delete the account's only workspace" });
|
|
4939
5044
|
}
|
|
4940
5045
|
if (input.activeSessionCount > 0) {
|
|
4941
|
-
throw new
|
|
5046
|
+
throw new HTTPException12(409, {
|
|
4942
5047
|
message: "stop the workspace's running sessions before deleting it"
|
|
4943
5048
|
});
|
|
4944
5049
|
}
|
|
@@ -4962,7 +5067,7 @@ import {
|
|
|
4962
5067
|
saveNewSessionDraftInTransaction,
|
|
4963
5068
|
withWorkspaceSubjectRls as withWorkspaceSubjectRls2
|
|
4964
5069
|
} from "@opengeni/db";
|
|
4965
|
-
import { HTTPException as
|
|
5070
|
+
import { HTTPException as HTTPException13 } from "hono/http-exception";
|
|
4966
5071
|
function hasOwn(value, key) {
|
|
4967
5072
|
return typeof value === "object" && value !== null && Object.hasOwn(value, key);
|
|
4968
5073
|
}
|
|
@@ -5084,7 +5189,7 @@ async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
|
|
|
5084
5189
|
const tools = toolsProvided ? validateToolRefs(input.tools, runtimeSettings) : [];
|
|
5085
5190
|
await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
|
|
5086
5191
|
if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
|
|
5087
|
-
throw new
|
|
5192
|
+
throw new HTTPException13(503, { message: "object storage is not configured" });
|
|
5088
5193
|
}
|
|
5089
5194
|
await validateFileResources(deps.db, workspaceId, resources);
|
|
5090
5195
|
assertConfiguredModel(deps.settings, input.model);
|
|
@@ -5118,7 +5223,7 @@ async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
|
|
|
5118
5223
|
return mapNewSessionDraft(saved);
|
|
5119
5224
|
} catch (error) {
|
|
5120
5225
|
if (error instanceof NewSessionDraftAccessError) {
|
|
5121
|
-
throw new
|
|
5226
|
+
throw new HTTPException13(403, { message: error.message });
|
|
5122
5227
|
}
|
|
5123
5228
|
throw error;
|
|
5124
5229
|
}
|
|
@@ -5656,8 +5761,12 @@ export {
|
|
|
5656
5761
|
getCapabilityPack,
|
|
5657
5762
|
getHumanComposerDraft,
|
|
5658
5763
|
hasPermission,
|
|
5764
|
+
hasReservedOpenGeniSlackBotMetadata,
|
|
5765
|
+
hasReservedOpenGeniSlackBotSessionMetadata,
|
|
5659
5766
|
isAuthoritativeGitHubRepositorySelectionError,
|
|
5660
5767
|
isBuiltInCapabilityPack,
|
|
5768
|
+
isOpenGeniSlackBotConnection,
|
|
5769
|
+
isTrustedScheduledSlackBotSession,
|
|
5661
5770
|
isUserMember,
|
|
5662
5771
|
listCapabilityPacks,
|
|
5663
5772
|
listFleet,
|
|
@@ -5672,6 +5781,7 @@ export {
|
|
|
5672
5781
|
moveHumanQueuePrompt,
|
|
5673
5782
|
normalizeResources,
|
|
5674
5783
|
officialMcpRegistryUrl,
|
|
5784
|
+
openGeniSlackBotMetadata,
|
|
5675
5785
|
postUserMessageTurn,
|
|
5676
5786
|
promoteSetupAppendChange,
|
|
5677
5787
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
@@ -5688,6 +5798,7 @@ export {
|
|
|
5688
5798
|
requireAccessGrant,
|
|
5689
5799
|
requireEnvironmentEncryption,
|
|
5690
5800
|
requireLimit,
|
|
5801
|
+
requireOpenGeniSlackBotConnection,
|
|
5691
5802
|
requirePermission,
|
|
5692
5803
|
requireQueuedTurnForApi,
|
|
5693
5804
|
requireRigChangeForApi,
|
|
@@ -5706,6 +5817,7 @@ export {
|
|
|
5706
5817
|
runOnSandbox,
|
|
5707
5818
|
saveActorNewSessionDraft,
|
|
5708
5819
|
saveHumanComposerDraft,
|
|
5820
|
+
scheduledSlackBotConnectionId,
|
|
5709
5821
|
scheduledTaskTemporalScheduleId,
|
|
5710
5822
|
scheduledTaskToolsProvided,
|
|
5711
5823
|
scheduledTaskTriggerToken,
|
|
@@ -5731,6 +5843,7 @@ export {
|
|
|
5731
5843
|
validateGitHubRepositorySelectionShape,
|
|
5732
5844
|
validateGitHubRepositorySelectionShapes,
|
|
5733
5845
|
validateMcpCapabilityConnection,
|
|
5846
|
+
validateOpenGeniSlackBotConnectionSelection,
|
|
5734
5847
|
validateToolRefs,
|
|
5735
5848
|
validateToolRefsForSessionPolicy,
|
|
5736
5849
|
validateVariableSetAttachment,
|