@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.d.ts +61 -9
- package/dist/index.js +496 -129
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/application/new-session-drafts.ts +120 -4
- package/src/dependencies.ts +2 -0
- package/src/domain/resources.ts +9 -0
- package/src/domain/scheduled-tasks.ts +31 -1
- package/src/domain/session-tool-policy.ts +25 -13
- package/src/domain/sessions.ts +207 -0
- package/src/domain/slack-bot.ts +130 -0
- package/src/index.ts +1 -0
- package/src/sandbox/routing.ts +292 -225
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 {
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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 =
|
|
3206
|
-
toolRefs.filter(
|
|
3207
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3926
|
+
throw new HTTPException10(404, { message: "session turn not found" });
|
|
3823
3927
|
}
|
|
3824
3928
|
if (turn.status !== "queued") {
|
|
3825
|
-
throw new
|
|
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
|
|
3982
|
+
throw new HTTPException10(409, { message: error.message });
|
|
3879
3983
|
}
|
|
3880
3984
|
if (error instanceof Error && error.message.includes("cancelled")) {
|
|
3881
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4361
|
+
throw new HTTPException10(403, { message: error.message });
|
|
4253
4362
|
}
|
|
4254
4363
|
if (error instanceof SessionIdConflictError) {
|
|
4255
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5043
|
+
throw new HTTPException12(409, { message: "cannot delete the account's only workspace" });
|
|
4772
5044
|
}
|
|
4773
5045
|
if (input.activeSessionCount > 0) {
|
|
4774
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|