@opengeni/core 0.8.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +148 -6
- package/dist/index.js +827 -127
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/application/new-session-drafts.ts +126 -0
- package/src/application/session-commands.ts +2 -0
- package/src/dependencies.ts +2 -0
- package/src/domain/resources.ts +32 -1
- package/src/domain/scheduled-tasks.ts +27 -9
- package/src/domain/session-tool-policy.ts +211 -0
- package/src/domain/sessions.ts +369 -84
- package/src/index.ts +2 -0
- package/src/sandbox/fleet.ts +149 -39
- package/src/sandbox/routing.ts +261 -3
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getSandbox as getSandbox2,
|
|
10
10
|
listSandboxes,
|
|
11
11
|
readActiveSandbox as readActiveSandbox2,
|
|
12
|
+
readLease,
|
|
12
13
|
requireSession,
|
|
13
14
|
setActiveSandbox
|
|
14
15
|
} from "@opengeni/db";
|
|
@@ -21,8 +22,20 @@ import {
|
|
|
21
22
|
import { HTTPException } from "hono/http-exception";
|
|
22
23
|
|
|
23
24
|
// src/sandbox/routing.ts
|
|
24
|
-
import { getSandbox, readActiveSandbox } from "@opengeni/db";
|
|
25
25
|
import {
|
|
26
|
+
advanceWorkspaceGenerationForDirectRequest,
|
|
27
|
+
advanceWorkspaceGenerationForRetainedProcess,
|
|
28
|
+
getSandbox,
|
|
29
|
+
markWarmLeaseInstanceLost,
|
|
30
|
+
readActiveSandbox,
|
|
31
|
+
retainWorkspaceMutationProcess,
|
|
32
|
+
settleRetainedProcess,
|
|
33
|
+
verifyDirectWorkspaceMutationSettlement,
|
|
34
|
+
verifyRetainedProcessMutationSettlement
|
|
35
|
+
} from "@opengeni/db";
|
|
36
|
+
import { appendAndPublishEvents } from "@opengeni/events";
|
|
37
|
+
import {
|
|
38
|
+
isProviderSandboxGoneDuringRoutedOperation,
|
|
26
39
|
makeActiveBackendResolver,
|
|
27
40
|
NatsControlRpc,
|
|
28
41
|
RoutingSandboxSession
|
|
@@ -63,6 +76,129 @@ function routingEnabled(settings) {
|
|
|
63
76
|
}
|
|
64
77
|
function wrapChannelABoxWithRouting(services, ids, established) {
|
|
65
78
|
const { db, settings, bus } = services;
|
|
79
|
+
const beforeMutation = async ({
|
|
80
|
+
op,
|
|
81
|
+
backend
|
|
82
|
+
}) => {
|
|
83
|
+
if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (backend.activeEpoch === void 0) {
|
|
87
|
+
throw new Error("API-direct workspace mutation resolved without an active route epoch");
|
|
88
|
+
}
|
|
89
|
+
return await advanceWorkspaceGenerationForDirectRequest(db, {
|
|
90
|
+
accountId: ids.accountId,
|
|
91
|
+
workspaceId: ids.workspaceId,
|
|
92
|
+
sessionId: ids.sessionId,
|
|
93
|
+
requestId: ids.directRequest.requestId,
|
|
94
|
+
holderId: ids.directRequest.holderId,
|
|
95
|
+
sandboxGroupId: ids.homeLease.sandboxGroupId,
|
|
96
|
+
expectedEpoch: backend.leaseEpoch,
|
|
97
|
+
expectedInstanceId: backend.providerInstanceId,
|
|
98
|
+
routeTargetId: backend.sandboxId,
|
|
99
|
+
routeEpoch: backend.activeEpoch,
|
|
100
|
+
operation: op
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const afterMutation = async ({
|
|
104
|
+
op,
|
|
105
|
+
backend,
|
|
106
|
+
admission,
|
|
107
|
+
outcome,
|
|
108
|
+
retainedProcess
|
|
109
|
+
}) => {
|
|
110
|
+
if (admission === null) return;
|
|
111
|
+
if (!admission || typeof admission !== "object" || typeof admission.id !== "string" || typeof admission.workspaceGeneration !== "number" || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0 || backend.activeEpoch === void 0) {
|
|
112
|
+
throw new Error("API-direct workspace mutation settlement lacked its exact admission");
|
|
113
|
+
}
|
|
114
|
+
const exactAdmission = admission;
|
|
115
|
+
if (outcome === "resolved" && retainedProcess) {
|
|
116
|
+
await retainWorkspaceMutationProcess(db, {
|
|
117
|
+
accountId: ids.accountId,
|
|
118
|
+
workspaceId: ids.workspaceId,
|
|
119
|
+
sessionId: ids.sessionId,
|
|
120
|
+
processId: retainedProcess.id,
|
|
121
|
+
providerSessionId: retainedProcess.providerSessionId,
|
|
122
|
+
admissionId: exactAdmission.id,
|
|
123
|
+
admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
|
|
124
|
+
operation: op,
|
|
125
|
+
owner: {
|
|
126
|
+
kind: "direct",
|
|
127
|
+
requestId: ids.directRequest.requestId,
|
|
128
|
+
holderId: ids.directRequest.holderId,
|
|
129
|
+
sandboxGroupId: ids.homeLease.sandboxGroupId,
|
|
130
|
+
expectedEpoch: backend.leaseEpoch,
|
|
131
|
+
expectedInstanceId: backend.providerInstanceId,
|
|
132
|
+
routeTargetId: exactAdmission.routeTargetId,
|
|
133
|
+
routeEpoch: exactAdmission.routeEpoch
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await verifyDirectWorkspaceMutationSettlement(db, {
|
|
139
|
+
accountId: ids.accountId,
|
|
140
|
+
workspaceId: ids.workspaceId,
|
|
141
|
+
sessionId: ids.sessionId,
|
|
142
|
+
requestId: ids.directRequest.requestId,
|
|
143
|
+
holderId: ids.directRequest.holderId,
|
|
144
|
+
sandboxGroupId: ids.homeLease.sandboxGroupId,
|
|
145
|
+
expectedEpoch: backend.leaseEpoch,
|
|
146
|
+
expectedInstanceId: backend.providerInstanceId,
|
|
147
|
+
routeTargetId: exactAdmission.routeTargetId,
|
|
148
|
+
routeEpoch: exactAdmission.routeEpoch,
|
|
149
|
+
admission: exactAdmission,
|
|
150
|
+
operation: op,
|
|
151
|
+
outcome
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const beforeProcessMutation = async ({
|
|
155
|
+
op,
|
|
156
|
+
process
|
|
157
|
+
}) => await advanceWorkspaceGenerationForRetainedProcess(db, {
|
|
158
|
+
accountId: ids.accountId,
|
|
159
|
+
workspaceId: ids.workspaceId,
|
|
160
|
+
sessionId: ids.sessionId,
|
|
161
|
+
processId: process.id,
|
|
162
|
+
operation: op
|
|
163
|
+
});
|
|
164
|
+
const afterProcessMutation = async ({
|
|
165
|
+
op,
|
|
166
|
+
process,
|
|
167
|
+
admission,
|
|
168
|
+
outcome
|
|
169
|
+
}) => {
|
|
170
|
+
if (!admission || typeof admission !== "object" || typeof admission.id !== "string" || typeof admission.workspaceGeneration !== "number") {
|
|
171
|
+
throw new Error("API retained-process mutation settlement lacked its exact admission");
|
|
172
|
+
}
|
|
173
|
+
await verifyRetainedProcessMutationSettlement(db, {
|
|
174
|
+
accountId: ids.accountId,
|
|
175
|
+
workspaceId: ids.workspaceId,
|
|
176
|
+
sessionId: ids.sessionId,
|
|
177
|
+
processId: process.id,
|
|
178
|
+
admission,
|
|
179
|
+
operation: op,
|
|
180
|
+
outcome
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const settleProcess = async ({
|
|
184
|
+
backend,
|
|
185
|
+
process,
|
|
186
|
+
proof
|
|
187
|
+
}) => {
|
|
188
|
+
if (backend.sandboxId !== null || backend.leaseEpoch === void 0 || backend.providerInstanceId === void 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await settleRetainedProcess(db, {
|
|
192
|
+
accountId: ids.accountId,
|
|
193
|
+
workspaceId: ids.workspaceId,
|
|
194
|
+
sessionId: ids.sessionId,
|
|
195
|
+
processId: process.id,
|
|
196
|
+
outcome: proof.outcome,
|
|
197
|
+
exitCode: proof.exitCode,
|
|
198
|
+
reason: proof.reason,
|
|
199
|
+
idleGraceMs: settings.sandboxIdleGraceMs
|
|
200
|
+
});
|
|
201
|
+
};
|
|
66
202
|
const resolver = makeActiveBackendResolver({
|
|
67
203
|
workspaceId: ids.workspaceId,
|
|
68
204
|
defaultBackend: established.session,
|
|
@@ -77,14 +213,61 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
77
213
|
} : null;
|
|
78
214
|
},
|
|
79
215
|
controlRpcFactory: controlRpcFactory(bus),
|
|
80
|
-
relay: relayConfigFromSettings(settings)
|
|
216
|
+
relay: relayConfigFromSettings(settings),
|
|
217
|
+
resolveDefaultBackend: async () => ({
|
|
218
|
+
session: established.session,
|
|
219
|
+
sandboxId: null,
|
|
220
|
+
kind: established.backendId,
|
|
221
|
+
leaseEpoch: ids.homeLease.leaseEpoch,
|
|
222
|
+
providerInstanceId: ids.homeLease.instanceId
|
|
223
|
+
})
|
|
81
224
|
});
|
|
82
225
|
const proxy = new RoutingSandboxSession({
|
|
226
|
+
defaultResolved: {
|
|
227
|
+
session: established.session,
|
|
228
|
+
sandboxId: null,
|
|
229
|
+
kind: established.backendId,
|
|
230
|
+
leaseEpoch: ids.homeLease.leaseEpoch,
|
|
231
|
+
providerInstanceId: ids.homeLease.instanceId
|
|
232
|
+
},
|
|
83
233
|
readPointer: async () => {
|
|
234
|
+
if (!routingEnabled(settings)) {
|
|
235
|
+
return { activeSandboxId: null, activeEpoch: 0 };
|
|
236
|
+
}
|
|
84
237
|
const pointer = await readActiveSandbox(db, ids.workspaceId, ids.sessionId);
|
|
85
238
|
return pointer ?? { activeSandboxId: null, activeEpoch: 0 };
|
|
86
239
|
},
|
|
87
|
-
resolveActiveBackend: resolver
|
|
240
|
+
resolveActiveBackend: resolver,
|
|
241
|
+
beforeMutation,
|
|
242
|
+
afterMutation,
|
|
243
|
+
beforeProcessMutation,
|
|
244
|
+
afterProcessMutation,
|
|
245
|
+
settleProcess,
|
|
246
|
+
onDefaultBackendError: async ({ error }) => {
|
|
247
|
+
if (!isProviderSandboxGoneDuringRoutedOperation(ids.homeLease.backend, error)) return null;
|
|
248
|
+
const marked = await markWarmLeaseInstanceLost(db, {
|
|
249
|
+
accountId: ids.accountId,
|
|
250
|
+
workspaceId: ids.workspaceId,
|
|
251
|
+
sandboxGroupId: ids.homeLease.sandboxGroupId,
|
|
252
|
+
expectedEpoch: ids.homeLease.leaseEpoch,
|
|
253
|
+
expectedInstanceId: ids.homeLease.instanceId,
|
|
254
|
+
diagnostic: "provider_not_found_during_routed_operation"
|
|
255
|
+
});
|
|
256
|
+
if (marked.status === "marked" && bus) {
|
|
257
|
+
await appendAndPublishEvents(db, bus, ids.workspaceId, ids.sessionId, [
|
|
258
|
+
{
|
|
259
|
+
type: "sandbox.box.lost",
|
|
260
|
+
payload: { sandboxId: ids.homeLease.instanceId }
|
|
261
|
+
}
|
|
262
|
+
]).catch(() => void 0);
|
|
263
|
+
}
|
|
264
|
+
const lease = marked.lease;
|
|
265
|
+
const restore = lease?.recovery.restore.status;
|
|
266
|
+
return {
|
|
267
|
+
leaseEpoch: lease?.leaseEpoch ?? ids.homeLease.leaseEpoch,
|
|
268
|
+
recovery: marked.status === "stale" ? "superseded" : restore === "pending" ? "pending" : restore === "degraded" ? "degraded" : "unrecoverable"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
88
271
|
});
|
|
89
272
|
return { ...established, session: proxy };
|
|
90
273
|
}
|
|
@@ -153,15 +336,33 @@ async function listFleet(services, ctx) {
|
|
|
153
336
|
};
|
|
154
337
|
const entries = [];
|
|
155
338
|
const groupActive = pointer.activeSandboxId === null;
|
|
339
|
+
const groupLease = await readLease(db, ctx.workspaceId, ctx.sessionGroupId);
|
|
340
|
+
const groupOnline = Boolean(
|
|
341
|
+
groupLease?.liveness === "warm" && groupLease.recovery.provider.status === "exists" && groupLease.recovery.workspace.status === "ready"
|
|
342
|
+
);
|
|
343
|
+
const groupRecovering = Boolean(
|
|
344
|
+
groupLease && (groupLease.liveness === "warming" || groupLease.recovery.restore.status === "pending" || groupLease.recovery.restore.status === "restoring" || groupLease.recovery.restore.status === "verifying")
|
|
345
|
+
);
|
|
156
346
|
entries.push({
|
|
157
347
|
id: ctx.sessionGroupId,
|
|
158
348
|
kind: ctx.sessionBackend === "selfhosted" ? "selfhosted" : "modal",
|
|
159
349
|
name: "session sandbox",
|
|
160
|
-
liveness: "online",
|
|
350
|
+
liveness: groupOnline ? "online" : groupRecovering ? "reconnecting" : "offline",
|
|
161
351
|
active: groupActive,
|
|
162
352
|
isSessionGroup: true,
|
|
163
353
|
enrollmentId: null,
|
|
164
|
-
attachable:
|
|
354
|
+
attachable: groupOnline,
|
|
355
|
+
providerStatus: groupLease?.recovery.provider.status ?? "not_created",
|
|
356
|
+
leaseLiveness: groupLease?.liveness ?? null,
|
|
357
|
+
routeStatus: groupActive ? "attached" : "detached",
|
|
358
|
+
archiveStatus: groupLease?.recovery.archive.status ?? "none",
|
|
359
|
+
restoreStatus: groupLease?.recovery.restore.status ?? "not_required",
|
|
360
|
+
workspaceStatus: groupLease?.recovery.workspace.status ?? "unknown",
|
|
361
|
+
leaseEpoch: groupLease?.leaseEpoch ?? null,
|
|
362
|
+
routeEpoch: pointer.activeEpoch,
|
|
363
|
+
workspaceGeneration: groupLease?.workspaceGeneration ?? null,
|
|
364
|
+
archiveGeneration: groupLease?.archiveGeneration ?? null,
|
|
365
|
+
archiveComplete: groupLease?.archiveComplete ?? false
|
|
165
366
|
});
|
|
166
367
|
const sandboxes = await listSandboxes(db, ctx.workspaceId);
|
|
167
368
|
for (const sandbox of sandboxes) {
|
|
@@ -181,7 +382,18 @@ async function listFleet(services, ctx) {
|
|
|
181
382
|
attachable: probe.liveness === "online",
|
|
182
383
|
consented: probe.consented,
|
|
183
384
|
hasDisplay: probe.hasDisplay,
|
|
184
|
-
lastSeenAt: enrollment?.lastSeenAt ?? null
|
|
385
|
+
lastSeenAt: enrollment?.lastSeenAt ?? null,
|
|
386
|
+
providerStatus: probe.liveness === "online" ? "exists" : probe.liveness === "reconnecting" ? "unknown" : "missing",
|
|
387
|
+
leaseLiveness: null,
|
|
388
|
+
routeStatus: pointer.activeSandboxId === sandbox.id ? "attached" : "detached",
|
|
389
|
+
archiveStatus: "none",
|
|
390
|
+
restoreStatus: "not_required",
|
|
391
|
+
workspaceStatus: probe.liveness === "online" ? "ready" : "not_ready",
|
|
392
|
+
leaseEpoch: null,
|
|
393
|
+
routeEpoch: pointer.activeEpoch,
|
|
394
|
+
workspaceGeneration: null,
|
|
395
|
+
archiveGeneration: null,
|
|
396
|
+
archiveComplete: false
|
|
185
397
|
});
|
|
186
398
|
}
|
|
187
399
|
return {
|
|
@@ -239,57 +451,75 @@ async function resolveTarget(services, ctx, target) {
|
|
|
239
451
|
async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
240
452
|
const resolved = await resolveTarget(services, ctx, target);
|
|
241
453
|
if (!resolved.ok) {
|
|
242
|
-
const
|
|
454
|
+
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
243
455
|
activeSandboxId: null,
|
|
244
456
|
activeEpoch: 0
|
|
245
457
|
};
|
|
246
458
|
return {
|
|
247
459
|
swapped: false,
|
|
248
|
-
activeSandboxId:
|
|
249
|
-
activeEpoch:
|
|
460
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
461
|
+
activeEpoch: pointer.activeEpoch,
|
|
250
462
|
reason: resolved.reason,
|
|
251
463
|
code: resolved.code
|
|
252
464
|
};
|
|
253
465
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
466
|
+
let readinessHold;
|
|
467
|
+
if (resolved.targetSandboxId === null && services.ensureSessionGroupReady) {
|
|
468
|
+
try {
|
|
469
|
+
readinessHold = await services.ensureSessionGroupReady(ctx);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const lease = await readLease(services.db, ctx.workspaceId, ctx.sessionGroupId);
|
|
472
|
+
const restore = lease?.recovery.restore.status;
|
|
473
|
+
const code = restore === "degraded" ? "recovery_degraded" : restore === "unrecoverable" ? "recovery_unrecoverable" : "recovery_in_progress";
|
|
474
|
+
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
475
|
+
activeSandboxId: null,
|
|
476
|
+
activeEpoch: 0
|
|
477
|
+
};
|
|
260
478
|
return {
|
|
261
|
-
swapped:
|
|
262
|
-
activeSandboxId:
|
|
263
|
-
activeEpoch:
|
|
479
|
+
swapped: false,
|
|
480
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
481
|
+
activeEpoch: pointer.activeEpoch,
|
|
482
|
+
reason: error instanceof Error ? error.message : "session sandbox did not reach verified readiness",
|
|
483
|
+
code
|
|
264
484
|
};
|
|
265
485
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
...workingDir !== void 0 ? { workingDir } : {}
|
|
273
|
-
});
|
|
274
|
-
if (result.swapped && result.pointer) {
|
|
275
|
-
return {
|
|
276
|
-
swapped: true,
|
|
277
|
-
activeSandboxId: result.pointer.activeSandboxId,
|
|
278
|
-
activeEpoch: result.pointer.activeEpoch
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
489
|
+
const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
490
|
+
activeSandboxId: null,
|
|
491
|
+
activeEpoch: 0
|
|
279
492
|
};
|
|
493
|
+
const result = await setActiveSandbox(services.db, {
|
|
494
|
+
accountId: ctx.accountId,
|
|
495
|
+
workspaceId: ctx.workspaceId,
|
|
496
|
+
sessionId: ctx.sessionId,
|
|
497
|
+
targetSandboxId: resolved.targetSandboxId,
|
|
498
|
+
expectedEpoch: pointer2.activeEpoch,
|
|
499
|
+
...workingDir !== void 0 ? { workingDir } : {}
|
|
500
|
+
});
|
|
501
|
+
if (result.swapped && result.pointer) {
|
|
502
|
+
return {
|
|
503
|
+
swapped: true,
|
|
504
|
+
activeSandboxId: result.pointer.activeSandboxId,
|
|
505
|
+
activeEpoch: result.pointer.activeEpoch
|
|
506
|
+
};
|
|
507
|
+
}
|
|
280
508
|
}
|
|
509
|
+
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
510
|
+
activeSandboxId: null,
|
|
511
|
+
activeEpoch: 0
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
swapped: false,
|
|
515
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
516
|
+
activeEpoch: pointer.activeEpoch,
|
|
517
|
+
reason: "a concurrent swap won the epoch fence; re-read and retry",
|
|
518
|
+
code: "concurrent_swap"
|
|
519
|
+
};
|
|
520
|
+
} finally {
|
|
521
|
+
await readinessHold?.release().catch(() => void 0);
|
|
281
522
|
}
|
|
282
|
-
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
283
|
-
activeSandboxId: null,
|
|
284
|
-
activeEpoch: 0
|
|
285
|
-
};
|
|
286
|
-
return {
|
|
287
|
-
swapped: false,
|
|
288
|
-
activeSandboxId: pointer.activeSandboxId,
|
|
289
|
-
activeEpoch: pointer.activeEpoch,
|
|
290
|
-
reason: "a concurrent swap won the epoch fence; re-read and retry",
|
|
291
|
-
code: "concurrent_swap"
|
|
292
|
-
};
|
|
293
523
|
}
|
|
294
524
|
async function runOnSandbox(services, ctx, target, op) {
|
|
295
525
|
const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
|
|
@@ -2698,6 +2928,22 @@ function enabledCapabilityMcpToolRefs(settings, runtimeSettings) {
|
|
|
2698
2928
|
function withDefaultEnabledCapabilityMcpTools(tools, settings, runtimeSettings) {
|
|
2699
2929
|
return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
|
|
2700
2930
|
}
|
|
2931
|
+
function availableToolRefs(tools, settings) {
|
|
2932
|
+
const available = new Set(settings.mcpServers.map((server) => server.id));
|
|
2933
|
+
return tools.filter((tool) => available.has(tool.id));
|
|
2934
|
+
}
|
|
2935
|
+
function assertToolRefsSubset(requested, allowed, message = "requested tools exceed the session tool policy") {
|
|
2936
|
+
const allowedIds = new Set(allowed.map((tool) => `${tool.kind}:${tool.id}`));
|
|
2937
|
+
const widened = requested.find((tool) => !allowedIds.has(`${tool.kind}:${tool.id}`));
|
|
2938
|
+
if (widened) {
|
|
2939
|
+
throw new HTTPException8(403, { message: `${message}: ${widened.id}` });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function validateToolRefsForSessionPolicy(input) {
|
|
2943
|
+
const validated = validateToolRefs(input.requested, input.settings);
|
|
2944
|
+
assertToolRefsSubset(validated, input.allowedTools, input.message);
|
|
2945
|
+
return validated;
|
|
2946
|
+
}
|
|
2701
2947
|
function normalizeResources(resources) {
|
|
2702
2948
|
const mountPaths = /* @__PURE__ */ new Map();
|
|
2703
2949
|
const identities = /* @__PURE__ */ new Map();
|
|
@@ -2902,29 +3148,161 @@ function positiveInteger(value) {
|
|
|
2902
3148
|
return null;
|
|
2903
3149
|
}
|
|
2904
3150
|
|
|
3151
|
+
// src/domain/session-tool-policy.ts
|
|
3152
|
+
import {
|
|
3153
|
+
SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT,
|
|
3154
|
+
SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH,
|
|
3155
|
+
mergeToolRefs as mergeToolRefs2
|
|
3156
|
+
} from "@opengeni/contracts";
|
|
3157
|
+
var MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"];
|
|
3158
|
+
var PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
|
|
3159
|
+
function sortedIds(ids) {
|
|
3160
|
+
return [...new Set(ids)].sort();
|
|
3161
|
+
}
|
|
3162
|
+
function projectIds(ids) {
|
|
3163
|
+
const projectable = ids.filter(
|
|
3164
|
+
(id) => id.length <= SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH && PROJECTABLE_REGISTRY_ID.test(id)
|
|
3165
|
+
);
|
|
3166
|
+
return {
|
|
3167
|
+
ids: projectable.slice(0, SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT),
|
|
3168
|
+
truncated: projectable.length !== ids.length || projectable.length > SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
function resolveSessionToolPolicy(input) {
|
|
3172
|
+
const policy = input.toolPolicy ?? { mode: "legacy", inheritedFromSessionId: null };
|
|
3173
|
+
const availableIds = new Set(input.availableMcpServerIds);
|
|
3174
|
+
const defaultIds = new Set(input.defaultMcpServerIds ?? []);
|
|
3175
|
+
const mandatoryIds = MANDATORY_SESSION_MCP_SERVER_IDS.filter(
|
|
3176
|
+
(id) => availableIds.has(id)
|
|
3177
|
+
);
|
|
3178
|
+
const mandatoryIdSet = new Set(mandatoryIds);
|
|
3179
|
+
const selectedRefs = input.turnToolsProvided === true ? mergeToolRefs2([], input.turnTools ?? []) : input.turnToolsProvided === false ? mergeToolRefs2([], input.sessionTools) : mergeToolRefs2(input.sessionTools, input.turnTools ?? []);
|
|
3180
|
+
const tracksWorkspaceDefaults = policy.mode === "workspace_default" && input.turnToolsProvided !== true;
|
|
3181
|
+
let toolRefs = selectedRefs.filter((tool) => tool.optional !== true || availableIds.has(tool.id));
|
|
3182
|
+
if (tracksWorkspaceDefaults) {
|
|
3183
|
+
toolRefs = mergeToolRefs2(
|
|
3184
|
+
toolRefs,
|
|
3185
|
+
sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true }))
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
toolRefs = mergeToolRefs2(
|
|
3189
|
+
toolRefs,
|
|
3190
|
+
mandatoryIds.map((id) => ({ kind: "mcp", id }))
|
|
3191
|
+
);
|
|
3192
|
+
const requestedEffectiveRefs = mergeToolRefs2(
|
|
3193
|
+
selectedRefs,
|
|
3194
|
+
tracksWorkspaceDefaults ? sortedIds(defaultIds).filter((id) => availableIds.has(id)).map((id) => ({ kind: "mcp", id, optional: true })) : []
|
|
3195
|
+
);
|
|
3196
|
+
const effectiveIds = sortedIds(
|
|
3197
|
+
mergeToolRefs2(
|
|
3198
|
+
requestedEffectiveRefs,
|
|
3199
|
+
mandatoryIds.map((id) => ({ kind: "mcp", id }))
|
|
3200
|
+
).map((tool) => tool.id)
|
|
3201
|
+
);
|
|
3202
|
+
const configuredIds = effectiveIds.filter((id) => availableIds.has(id));
|
|
3203
|
+
const configuredIdSet = new Set(configuredIds);
|
|
3204
|
+
const droppedIds = effectiveIds.filter((id) => !configuredIdSet.has(id));
|
|
3205
|
+
const deferredIds = tracksWorkspaceDefaults ? sortedIds(
|
|
3206
|
+
toolRefs.filter(
|
|
3207
|
+
(tool) => tool.optional === true && configuredIdSet.has(tool.id) && !mandatoryIdSet.has(tool.id)
|
|
3208
|
+
).map((tool) => tool.id)
|
|
3209
|
+
) : [];
|
|
3210
|
+
const selectedIds = sortedIds(
|
|
3211
|
+
selectedRefs.filter(
|
|
3212
|
+
(tool) => !mandatoryIdSet.has(tool.id) && !(tracksWorkspaceDefaults && tool.optional === true)
|
|
3213
|
+
).map((tool) => tool.id)
|
|
3214
|
+
);
|
|
3215
|
+
const projections = {
|
|
3216
|
+
selected: projectIds(selectedIds),
|
|
3217
|
+
effective: projectIds(effectiveIds),
|
|
3218
|
+
mandatory: projectIds(sortedIds(mandatoryIds)),
|
|
3219
|
+
deferred: projectIds(deferredIds),
|
|
3220
|
+
configured: projectIds(configuredIds),
|
|
3221
|
+
dropped: projectIds(droppedIds)
|
|
3222
|
+
};
|
|
3223
|
+
return {
|
|
3224
|
+
toolRefs,
|
|
3225
|
+
effectivePolicy: {
|
|
3226
|
+
mode: policy.mode,
|
|
3227
|
+
inheritedFromSessionId: policy.inheritedFromSessionId,
|
|
3228
|
+
selectedIds: projections.selected.ids,
|
|
3229
|
+
effectiveIds: projections.effective.ids,
|
|
3230
|
+
mandatoryIds: projections.mandatory.ids,
|
|
3231
|
+
lazyRouter: {
|
|
3232
|
+
state: tracksWorkspaceDefaults ? "required" : "disabled",
|
|
3233
|
+
deferredIds: projections.deferred.ids
|
|
3234
|
+
},
|
|
3235
|
+
configuredIds: projections.configured.ids,
|
|
3236
|
+
droppedIds: projections.dropped.ids,
|
|
3237
|
+
counts: {
|
|
3238
|
+
selected: selectedIds.length,
|
|
3239
|
+
effective: effectiveIds.length,
|
|
3240
|
+
mandatory: mandatoryIds.length,
|
|
3241
|
+
deferred: deferredIds.length,
|
|
3242
|
+
configured: configuredIds.length,
|
|
3243
|
+
dropped: droppedIds.length
|
|
3244
|
+
},
|
|
3245
|
+
idsTruncated: Object.values(projections).some((projection) => projection.truncated)
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
async function workspaceSessionToolPolicyServerIds(db, workspaceId, settings) {
|
|
3250
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
3251
|
+
return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
|
|
3252
|
+
}
|
|
3253
|
+
async function workspaceSessionToolPolicyDefaultServerIds(db, workspaceId, settings) {
|
|
3254
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
|
|
3255
|
+
return sortedIds(enabledCapabilityMcpToolRefs(settings, runtimeSettings).map((tool) => tool.id));
|
|
3256
|
+
}
|
|
3257
|
+
function sessionWithEffectiveToolPolicy(session, workspaceServerIds, workspaceDefaultServerIds = []) {
|
|
3258
|
+
const availableIds = new Set(workspaceServerIds);
|
|
3259
|
+
for (const server of session.mcpServers) {
|
|
3260
|
+
availableIds.add(server.id);
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
...session,
|
|
3264
|
+
effectiveToolPolicy: resolveSessionToolPolicy({
|
|
3265
|
+
...session.toolPolicy ? { toolPolicy: session.toolPolicy } : {},
|
|
3266
|
+
sessionTools: session.tools,
|
|
3267
|
+
availableMcpServerIds: availableIds,
|
|
3268
|
+
defaultMcpServerIds: workspaceDefaultServerIds
|
|
3269
|
+
}).effectivePolicy
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
|
|
2905
3273
|
// src/domain/scheduled-tasks.ts
|
|
2906
3274
|
import {
|
|
2907
3275
|
createScheduledTask,
|
|
2908
3276
|
deleteScheduledTask,
|
|
3277
|
+
getNestedAgentDepthDeploymentPolicy,
|
|
2909
3278
|
getRig as getRig3,
|
|
2910
3279
|
getScheduledTask,
|
|
3280
|
+
requireWorkspace as requireWorkspace2,
|
|
2911
3281
|
updateScheduledTask
|
|
2912
3282
|
} from "@opengeni/db";
|
|
2913
3283
|
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
2914
3284
|
|
|
2915
3285
|
// src/domain/sessions.ts
|
|
2916
3286
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
2917
|
-
import {
|
|
3287
|
+
import {
|
|
3288
|
+
canonicalizeConfiguredModelId,
|
|
3289
|
+
configuredAllowedModels,
|
|
3290
|
+
policyProviderIdForModel,
|
|
3291
|
+
resolveTurnExecutionPolicyV1
|
|
3292
|
+
} from "@opengeni/config";
|
|
2918
3293
|
import {
|
|
2919
3294
|
CreateSessionRequest,
|
|
3295
|
+
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
3296
|
+
SessionSpawnDenial,
|
|
2920
3297
|
ServiceTurnInitiator,
|
|
2921
3298
|
ServiceTurnInitiatorContext,
|
|
2922
3299
|
evaluateWorkspaceModelPolicy,
|
|
2923
|
-
reasoningEffortForMetadata
|
|
3300
|
+
reasoningEffortForMetadata,
|
|
3301
|
+
SessionMcpApprovalPolicy
|
|
2924
3302
|
} from "@opengeni/contracts";
|
|
2925
3303
|
import {
|
|
2926
3304
|
createSession,
|
|
2927
|
-
|
|
3305
|
+
createSessionWithIdempotencyKeyResult,
|
|
2928
3306
|
encryptVariableSetValue as encryptVariableSetValue2,
|
|
2929
3307
|
getAnySessionInGroup,
|
|
2930
3308
|
getEnrollment as getEnrollment2,
|
|
@@ -2935,7 +3313,7 @@ import {
|
|
|
2935
3313
|
getSandbox as getSandbox3,
|
|
2936
3314
|
getSession as getSession2,
|
|
2937
3315
|
SessionIdConflictError,
|
|
2938
|
-
|
|
3316
|
+
getSessionSpawnDenialByIdempotencyKey,
|
|
2939
3317
|
getSessionEvent,
|
|
2940
3318
|
getWorkspaceControlEvent,
|
|
2941
3319
|
getSessionLineage,
|
|
@@ -2946,14 +3324,16 @@ import {
|
|
|
2946
3324
|
listSessionMcpServersForChildInheritance,
|
|
2947
3325
|
requireSession as requireSession2,
|
|
2948
3326
|
submitHumanPromptInTransaction,
|
|
3327
|
+
appendSessionEventsWithLockedSessionUpdate,
|
|
2949
3328
|
updateSessionTitle as updateSessionTitleRow,
|
|
2950
3329
|
withWorkspaceSubjectRls,
|
|
2951
3330
|
QueueCommandConflictError,
|
|
2952
3331
|
AgentCommandAuthorityError,
|
|
3332
|
+
SessionSpawnDeniedDbError,
|
|
2953
3333
|
SessionControlConflictError
|
|
2954
3334
|
} from "@opengeni/db";
|
|
2955
3335
|
import {
|
|
2956
|
-
appendAndPublishEvents,
|
|
3336
|
+
appendAndPublishEvents as appendAndPublishEvents2,
|
|
2957
3337
|
publishDurableSessionEvents,
|
|
2958
3338
|
publishDurableWorkspaceControlEvent
|
|
2959
3339
|
} from "@opengeni/events";
|
|
@@ -2962,6 +3342,29 @@ var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files",
|
|
|
2962
3342
|
var maxSessionMcpCredentialHeaders = 16;
|
|
2963
3343
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
2964
3344
|
var sessionMcpCredentialHeaderName = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
3345
|
+
var SessionSpawnDeniedError = class extends Error {
|
|
3346
|
+
denial;
|
|
3347
|
+
constructor(denial) {
|
|
3348
|
+
super(sessionSpawnDeniedMessage(denial));
|
|
3349
|
+
this.name = "SessionSpawnDeniedError";
|
|
3350
|
+
this.denial = denial;
|
|
3351
|
+
}
|
|
3352
|
+
};
|
|
3353
|
+
function sessionSpawnDeniedMessage(denial) {
|
|
3354
|
+
if (denial.code === "nested_agent_depth_override_forbidden") {
|
|
3355
|
+
return `requested nested-agent depth limit ${denial.requestedMaxNestedAgentDepthOverride ?? "unknown"} exceeds inherited limit ${denial.effectiveMaxNestedAgentDepth}; workspace:admin is required to increase it`;
|
|
3356
|
+
}
|
|
3357
|
+
return `nested-agent depth ${denial.attemptedDepth} exceeds effective limit ${denial.effectiveMaxNestedAgentDepth} (current parent depth ${denial.currentDepth})`;
|
|
3358
|
+
}
|
|
3359
|
+
function sessionSpawnDenialEnvelope(error) {
|
|
3360
|
+
return {
|
|
3361
|
+
error: {
|
|
3362
|
+
code: error.denial.code,
|
|
3363
|
+
message: error.message,
|
|
3364
|
+
details: { denial: error.denial }
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
2965
3368
|
function serviceInitiatorForGrant(grant) {
|
|
2966
3369
|
if (!grant.serviceInitiator) {
|
|
2967
3370
|
if (grant.serviceInitiatorContext) {
|
|
@@ -3091,6 +3494,7 @@ function mcpServerConfigFromMetadata(server) {
|
|
|
3091
3494
|
...server.name ? { name: server.name } : {},
|
|
3092
3495
|
url: server.url,
|
|
3093
3496
|
cacheToolsList: false,
|
|
3497
|
+
requireApproval: server.requireApproval,
|
|
3094
3498
|
...server.connectionRef ? { connectionRef: server.connectionRef } : {}
|
|
3095
3499
|
};
|
|
3096
3500
|
}
|
|
@@ -3151,6 +3555,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
3151
3555
|
url: server.url,
|
|
3152
3556
|
headerNames: Object.keys(headersEncrypted).sort(),
|
|
3153
3557
|
credentialVersion: 1,
|
|
3558
|
+
requireApproval: server.requireApproval ?? false,
|
|
3154
3559
|
connectionRef: server.connectionRef ?? null
|
|
3155
3560
|
});
|
|
3156
3561
|
}
|
|
@@ -3186,6 +3591,7 @@ function validateInheritedSessionMcpServersForCreate(servers) {
|
|
|
3186
3591
|
url: server.url,
|
|
3187
3592
|
headerNames: Object.keys(server.headersEncrypted ?? {}).sort(),
|
|
3188
3593
|
credentialVersion: 1,
|
|
3594
|
+
requireApproval: server.requireApproval ?? false,
|
|
3189
3595
|
connectionRef: server.connectionRef ?? null
|
|
3190
3596
|
}))
|
|
3191
3597
|
};
|
|
@@ -3228,21 +3634,7 @@ async function createAndStartSession(input) {
|
|
|
3228
3634
|
reasoningEffort: input.reasoningEffort
|
|
3229
3635
|
};
|
|
3230
3636
|
if (input.createIdempotencyKey) {
|
|
3231
|
-
const
|
|
3232
|
-
input.db,
|
|
3233
|
-
input.workspaceId,
|
|
3234
|
-
input.createIdempotencyKey
|
|
3235
|
-
);
|
|
3236
|
-
if (existing) {
|
|
3237
|
-
if (input.requestedSessionId && existing.id !== input.requestedSessionId) {
|
|
3238
|
-
throw new SessionIdConflictError(input.requestedSessionId);
|
|
3239
|
-
}
|
|
3240
|
-
return await finishStartSession(
|
|
3241
|
-
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
3242
|
-
existing
|
|
3243
|
-
);
|
|
3244
|
-
}
|
|
3245
|
-
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
3637
|
+
const keyedResult = await createSessionWithIdempotencyKeyResult(input.db, {
|
|
3246
3638
|
...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
|
|
3247
3639
|
accountId: input.accountId,
|
|
3248
3640
|
workspaceId: input.workspaceId,
|
|
@@ -3250,6 +3642,7 @@ async function createAndStartSession(input) {
|
|
|
3250
3642
|
initialTurnInstructions: input.turnInstructions ?? null,
|
|
3251
3643
|
resources: input.resources,
|
|
3252
3644
|
tools: input.tools,
|
|
3645
|
+
...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
|
|
3253
3646
|
metadata: sessionMetadata,
|
|
3254
3647
|
...input.createdBy ? { createdBy: input.createdBy } : {},
|
|
3255
3648
|
...input.createdByContext ? { createdByContext: input.createdByContext } : {},
|
|
@@ -3265,8 +3658,15 @@ async function createAndStartSession(input) {
|
|
|
3265
3658
|
createIdempotencyKey: input.createIdempotencyKey,
|
|
3266
3659
|
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
3267
3660
|
...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
|
|
3268
|
-
mcpServers: input.mcpServers ?? []
|
|
3661
|
+
mcpServers: input.mcpServers ?? [],
|
|
3662
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
3663
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
3664
|
+
subjectId: input.subjectId ?? null
|
|
3269
3665
|
});
|
|
3666
|
+
if (keyedResult.denied) {
|
|
3667
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(keyedResult.denial));
|
|
3668
|
+
}
|
|
3669
|
+
const { session: keyed, created } = keyedResult;
|
|
3270
3670
|
if (!created) {
|
|
3271
3671
|
return await finishStartSession(
|
|
3272
3672
|
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
@@ -3275,30 +3675,42 @@ async function createAndStartSession(input) {
|
|
|
3275
3675
|
}
|
|
3276
3676
|
return await finishStartSession(input, keyed);
|
|
3277
3677
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3678
|
+
let session;
|
|
3679
|
+
try {
|
|
3680
|
+
session = await createSession(input.db, {
|
|
3681
|
+
...input.requestedSessionId ? { requestedSessionId: input.requestedSessionId } : {},
|
|
3682
|
+
accountId: input.accountId,
|
|
3683
|
+
workspaceId: input.workspaceId,
|
|
3684
|
+
initialMessage: input.initialMessage,
|
|
3685
|
+
initialTurnInstructions: input.turnInstructions ?? null,
|
|
3686
|
+
resources: input.resources,
|
|
3687
|
+
tools: input.tools,
|
|
3688
|
+
...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
|
|
3689
|
+
metadata: sessionMetadata,
|
|
3690
|
+
...input.createdBy ? { createdBy: input.createdBy } : {},
|
|
3691
|
+
...input.createdByContext ? { createdByContext: input.createdByContext } : {},
|
|
3692
|
+
createdByActor: input.createdByActor ?? null,
|
|
3693
|
+
model: input.model,
|
|
3694
|
+
sandboxBackend: input.sandboxBackend,
|
|
3695
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
3696
|
+
rigId: input.rigId ?? null,
|
|
3697
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
3698
|
+
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
3699
|
+
instructions: input.instructions ?? null,
|
|
3700
|
+
parentSessionId: input.parentSessionId ?? null,
|
|
3701
|
+
sandboxGroupId: input.sandboxGroupId ?? null,
|
|
3702
|
+
...input.sandboxOs ? { sandboxOs: input.sandboxOs } : {},
|
|
3703
|
+
mcpServers: input.mcpServers ?? [],
|
|
3704
|
+
maxNestedAgentDepthOverride: input.maxNestedAgentDepthOverride ?? null,
|
|
3705
|
+
allowNestedAgentDepthIncrease: input.allowNestedAgentDepthIncrease ?? false,
|
|
3706
|
+
subjectId: input.subjectId ?? null
|
|
3707
|
+
});
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
if (error instanceof SessionSpawnDeniedDbError) {
|
|
3710
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(error.denial));
|
|
3711
|
+
}
|
|
3712
|
+
throw error;
|
|
3713
|
+
}
|
|
3302
3714
|
return await finishStartSession(input, session);
|
|
3303
3715
|
}
|
|
3304
3716
|
async function finishStartSession(input, session) {
|
|
@@ -3335,7 +3747,9 @@ async function finishStartSession(input, session) {
|
|
|
3335
3747
|
sessionId: session.id,
|
|
3336
3748
|
...input.clientEventId ? { clientEventId: input.clientEventId } : {},
|
|
3337
3749
|
reasoningEffortFallback: input.reasoningEffort,
|
|
3750
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
3338
3751
|
createdEventPayload: {
|
|
3752
|
+
...input.toolPolicy ? { toolPolicy: input.toolPolicy } : {},
|
|
3339
3753
|
...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
|
|
3340
3754
|
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
3341
3755
|
},
|
|
@@ -3343,7 +3757,8 @@ async function finishStartSession(input, session) {
|
|
|
3343
3757
|
text: input.goal.text,
|
|
3344
3758
|
...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
|
|
3345
3759
|
...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
|
|
3346
|
-
} : null
|
|
3760
|
+
} : null,
|
|
3761
|
+
consumeNewSessionDraft: input.consumeNewSessionDraft ?? null
|
|
3347
3762
|
});
|
|
3348
3763
|
await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
|
|
3349
3764
|
if (started.workflowWakeRevision !== null) {
|
|
@@ -3362,31 +3777,42 @@ async function finishStartSession(input, session) {
|
|
|
3362
3777
|
function workflowIdForSession(sessionId) {
|
|
3363
3778
|
return `session-${sessionId}`;
|
|
3364
3779
|
}
|
|
3365
|
-
function
|
|
3780
|
+
function canonicalConfiguredModel(settings, model) {
|
|
3366
3781
|
if (model === null || model === void 0) {
|
|
3367
|
-
return;
|
|
3782
|
+
return model;
|
|
3368
3783
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3784
|
+
const canonicalModel = canonicalizeConfiguredModelId(settings, model);
|
|
3785
|
+
if (configuredAllowedModels(settings).includes(canonicalModel)) {
|
|
3786
|
+
return canonicalModel;
|
|
3371
3787
|
}
|
|
3372
|
-
if (settings.codexSubscriptionEnabled &&
|
|
3373
|
-
return;
|
|
3788
|
+
if (settings.codexSubscriptionEnabled && canonicalModel.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
3789
|
+
return canonicalModel;
|
|
3374
3790
|
}
|
|
3375
3791
|
throw new HTTPException9(422, { message: `model is not available: ${model}` });
|
|
3376
3792
|
}
|
|
3793
|
+
function assertConfiguredModel(settings, model) {
|
|
3794
|
+
canonicalConfiguredModel(settings, model);
|
|
3795
|
+
}
|
|
3377
3796
|
async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
|
|
3378
3797
|
if (model === null || model === void 0) {
|
|
3379
3798
|
return;
|
|
3380
3799
|
}
|
|
3800
|
+
const canonicalModel = canonicalConfiguredModel(settings, model);
|
|
3801
|
+
if (canonicalModel === null || canonicalModel === void 0) {
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3381
3804
|
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
3382
3805
|
if (!policy) {
|
|
3383
3806
|
return;
|
|
3384
3807
|
}
|
|
3385
|
-
const providerId = policyProviderIdForModel(settings,
|
|
3386
|
-
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
3808
|
+
const providerId = policyProviderIdForModel(settings, canonicalModel);
|
|
3809
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, {
|
|
3810
|
+
providerId,
|
|
3811
|
+
modelId: canonicalModel
|
|
3812
|
+
});
|
|
3387
3813
|
if (!verdict.allowed) {
|
|
3388
3814
|
throw new HTTPException9(422, {
|
|
3389
|
-
message: verdict.reason === "provider" ? `model "${
|
|
3815
|
+
message: verdict.reason === "provider" ? `model "${canonicalModel}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${canonicalModel}" is not allowed by this workspace's model policy`
|
|
3390
3816
|
});
|
|
3391
3817
|
}
|
|
3392
3818
|
}
|
|
@@ -3407,7 +3833,7 @@ function reasoningEffortForSession(metadata, fallback) {
|
|
|
3407
3833
|
}
|
|
3408
3834
|
async function postUserMessageTurn(input) {
|
|
3409
3835
|
const { db, bus, workflowClient, settings, accountId, workspaceId, sessionId } = input;
|
|
3410
|
-
const requestedModel = input.model ?? null;
|
|
3836
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
3411
3837
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
3412
3838
|
assertConfiguredModel(settings, requestedModel);
|
|
3413
3839
|
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
@@ -3437,9 +3863,11 @@ async function postUserMessageTurn(input) {
|
|
|
3437
3863
|
turnInstructions: input.turnInstructions ?? null,
|
|
3438
3864
|
resources: input.resources,
|
|
3439
3865
|
tools: input.tools,
|
|
3866
|
+
toolsProvided: input.toolsProvided,
|
|
3440
3867
|
model: requestedModel,
|
|
3441
3868
|
reasoningEffort: requestedReasoningEffort,
|
|
3442
|
-
reasoningEffortFallback: settings.openaiReasoningEffort,
|
|
3869
|
+
reasoningEffortFallback: input.reasoningEffortFallback ?? settings.openaiReasoningEffort,
|
|
3870
|
+
turnExecutionPolicy: input.turnExecutionPolicy,
|
|
3443
3871
|
source: input.origin === "operator" ? "api" : "user",
|
|
3444
3872
|
mcpCredentialUpdates: input.mcpCredentialUpdates ?? []
|
|
3445
3873
|
})
|
|
@@ -3506,6 +3934,16 @@ async function postUserMessageTurn(input) {
|
|
|
3506
3934
|
async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
3507
3935
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
3508
3936
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
3937
|
+
if (payload.idempotencyKey) {
|
|
3938
|
+
const denial = await getSessionSpawnDenialByIdempotencyKey(
|
|
3939
|
+
db,
|
|
3940
|
+
workspaceId,
|
|
3941
|
+
payload.idempotencyKey
|
|
3942
|
+
);
|
|
3943
|
+
if (denial) {
|
|
3944
|
+
throw new SessionSpawnDeniedError(SessionSpawnDenial.parse(denial));
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3509
3947
|
const parentSessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
|
|
3510
3948
|
if (parentSessionId) {
|
|
3511
3949
|
await requireSessionAuthorization(deps, grant, {
|
|
@@ -3535,12 +3973,50 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3535
3973
|
const resources = normalizeResources(
|
|
3536
3974
|
hasOwnProperty(rawPayload, "resources") ? payload.resources : parentSession?.resources ?? payload.resources
|
|
3537
3975
|
);
|
|
3976
|
+
const toolsProvided = hasOwnProperty(rawPayload, "tools");
|
|
3538
3977
|
const requestedTools = validateToolRefs(
|
|
3539
|
-
|
|
3978
|
+
toolsProvided ? payload.tools : parentSession?.tools ?? payload.tools,
|
|
3540
3979
|
runtimeSettings
|
|
3541
3980
|
);
|
|
3542
|
-
|
|
3543
|
-
|
|
3981
|
+
let selectedTools;
|
|
3982
|
+
let toolPolicy;
|
|
3983
|
+
if (parentSession) {
|
|
3984
|
+
const parentTracksWorkspaceDefaults = parentSession.toolPolicy?.mode === "workspace_default";
|
|
3985
|
+
const parentEffective = withFirstPartyTools(
|
|
3986
|
+
parentTracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
|
|
3987
|
+
availableToolRefs(parentSession.tools, runtimeSettings),
|
|
3988
|
+
settings,
|
|
3989
|
+
runtimeSettings
|
|
3990
|
+
) : parentSession.tools,
|
|
3991
|
+
runtimeSettings
|
|
3992
|
+
);
|
|
3993
|
+
if (toolsProvided) {
|
|
3994
|
+
assertToolRefsSubset(
|
|
3995
|
+
requestedTools,
|
|
3996
|
+
parentEffective,
|
|
3997
|
+
"child tools may only narrow the parent session tool policy"
|
|
3998
|
+
);
|
|
3999
|
+
selectedTools = requestedTools;
|
|
4000
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: parentSession.id };
|
|
4001
|
+
} else {
|
|
4002
|
+
selectedTools = parentEffective;
|
|
4003
|
+
toolPolicy = {
|
|
4004
|
+
mode: parentTracksWorkspaceDefaults ? "workspace_default" : "inherited",
|
|
4005
|
+
inheritedFromSessionId: parentSession.id
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
} else if (toolsProvided) {
|
|
4009
|
+
selectedTools = requestedTools;
|
|
4010
|
+
toolPolicy = { mode: "explicit", inheritedFromSessionId: null };
|
|
4011
|
+
} else {
|
|
4012
|
+
selectedTools = withDefaultEnabledCapabilityMcpTools(
|
|
4013
|
+
requestedTools,
|
|
4014
|
+
settings,
|
|
4015
|
+
capabilityRuntimeSettings
|
|
4016
|
+
);
|
|
4017
|
+
toolPolicy = { mode: "workspace_default", inheritedFromSessionId: null };
|
|
4018
|
+
}
|
|
4019
|
+
const tools = withFirstPartyTools(selectedTools, runtimeSettings);
|
|
3544
4020
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
3545
4021
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3546
4022
|
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
@@ -3568,16 +4044,30 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3568
4044
|
frozenRigVersionId = rig.activeVersion.id;
|
|
3569
4045
|
}
|
|
3570
4046
|
}
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
payload.model ?? settings.openaiModel
|
|
3577
|
-
);
|
|
3578
|
-
const model = payload.model ?? settings.openaiModel;
|
|
4047
|
+
const model = canonicalConfiguredModel(settings, payload.model ?? settings.openaiModel);
|
|
4048
|
+
if (model === null || model === void 0) {
|
|
4049
|
+
throw new Error("effective session model unexpectedly resolved to null");
|
|
4050
|
+
}
|
|
4051
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model);
|
|
3579
4052
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
3580
|
-
|
|
4053
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
4054
|
+
modelId: model,
|
|
4055
|
+
requestedModelId: payload.model ?? null,
|
|
4056
|
+
modelSource: payload.model === void 0 ? "deployment" : "explicit",
|
|
4057
|
+
reasoningEffort,
|
|
4058
|
+
reasoningSource: payload.reasoningEffort === void 0 ? "deployment" : "explicit"
|
|
4059
|
+
});
|
|
4060
|
+
const parentFirstPartyMcpPermissions = parentSession ? [...parentSession.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS] : null;
|
|
4061
|
+
if (parentFirstPartyMcpPermissions && payload.firstPartyMcpPermissions?.some(
|
|
4062
|
+
(permission) => !hasPermission(parentFirstPartyMcpPermissions, permission)
|
|
4063
|
+
)) {
|
|
4064
|
+
throw new HTTPException9(403, {
|
|
4065
|
+
message: "child first-party MCP permissions may only narrow the parent session grant"
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? (parentFirstPartyMcpPermissions ? parentFirstPartyMcpPermissions.filter(
|
|
4069
|
+
(permission) => hasPermission(grant.permissions, permission)
|
|
4070
|
+
) : null);
|
|
3581
4071
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
3582
4072
|
throw new HTTPException9(422, {
|
|
3583
4073
|
message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
|
|
@@ -3710,9 +4200,11 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3710
4200
|
turnInstructions: payload.turnInstructions ?? null,
|
|
3711
4201
|
resources,
|
|
3712
4202
|
tools,
|
|
4203
|
+
toolPolicy,
|
|
3713
4204
|
...payload.clientEventId ? { clientEventId: payload.clientEventId } : {},
|
|
3714
4205
|
model,
|
|
3715
4206
|
reasoningEffort,
|
|
4207
|
+
turnExecutionPolicy,
|
|
3716
4208
|
// A shared spawn inherits the box's backend; a caller-supplied
|
|
3717
4209
|
// sandboxBackend on a shared spawn is ignored (it is the same box). A
|
|
3718
4210
|
// machine-targeted top-level create labels the home "selfhosted"
|
|
@@ -3742,11 +4234,18 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3742
4234
|
sessionMcpServers: sessionMcpServers.metadata,
|
|
3743
4235
|
parentSessionId,
|
|
3744
4236
|
createIdempotencyKey: payload.idempotencyKey ?? null,
|
|
4237
|
+
maxNestedAgentDepthOverride: payload.maxNestedAgentDepth ?? null,
|
|
4238
|
+
allowNestedAgentDepthIncrease: hasPermission(grant.permissions, "workspace:admin"),
|
|
4239
|
+
subjectId: grant.subjectId,
|
|
3745
4240
|
// Create-time machine targeting (A-2a): when a target sandbox is named, the
|
|
3746
4241
|
// active-sandbox pointer is seeded race-free inside createAndStartSession
|
|
3747
4242
|
// (after the row exists, before the first turn dispatches). Validation
|
|
3748
4243
|
// (ownership/liveness) lives in swapActiveSandbox; an invalid target 422s.
|
|
3749
|
-
seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null
|
|
4244
|
+
seedTargetSandbox: payload.targetSandboxId ? { sandboxId: payload.targetSandboxId, settings, workingDir: payload.workingDir ?? null } : null,
|
|
4245
|
+
consumeNewSessionDraft: payload.expectedNewSessionDraftRevision !== void 0 ? {
|
|
4246
|
+
subjectId: grant.subjectId,
|
|
4247
|
+
expectedRevision: payload.expectedNewSessionDraftRevision
|
|
4248
|
+
} : null
|
|
3750
4249
|
});
|
|
3751
4250
|
} catch (error) {
|
|
3752
4251
|
if (error instanceof AgentCommandAuthorityError) {
|
|
@@ -3777,6 +4276,11 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
3777
4276
|
return session;
|
|
3778
4277
|
}
|
|
3779
4278
|
async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
|
|
4279
|
+
if (input.toolsProvided && !deps.settings.sessionTurnToolReplacementEnabled) {
|
|
4280
|
+
throw new HTTPException9(503, {
|
|
4281
|
+
message: "explicit follow-up tool replacement is temporarily unavailable until provenance-aware turn workers finish rolling out; omit tools to inherit the session policy and retry"
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
3780
4284
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
3781
4285
|
await requireSessionAuthorization(deps, grant, {
|
|
3782
4286
|
sessionId,
|
|
@@ -3789,19 +4293,50 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
3789
4293
|
settings
|
|
3790
4294
|
);
|
|
3791
4295
|
const existingSession = await requireSession2(db, workspaceId, sessionId);
|
|
4296
|
+
const requestedModel = canonicalConfiguredModel(settings, input.model ?? null) ?? null;
|
|
4297
|
+
const effectiveModel = canonicalConfiguredModel(settings, requestedModel ?? existingSession.model) ?? null;
|
|
4298
|
+
if (effectiveModel === null) {
|
|
4299
|
+
throw new Error("effective follow-up model unexpectedly resolved to null");
|
|
4300
|
+
}
|
|
4301
|
+
const sessionReasoningEffort = reasoningEffortForSession(
|
|
4302
|
+
existingSession.metadata,
|
|
4303
|
+
settings.openaiReasoningEffort
|
|
4304
|
+
);
|
|
4305
|
+
const effectiveReasoningEffort = input.reasoningEffort ?? sessionReasoningEffort;
|
|
4306
|
+
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
4307
|
+
modelId: effectiveModel,
|
|
4308
|
+
requestedModelId: input.model ?? null,
|
|
4309
|
+
modelSource: input.model == null ? "session" : "explicit",
|
|
4310
|
+
reasoningEffort: effectiveReasoningEffort,
|
|
4311
|
+
reasoningSource: input.reasoningEffort == null ? "session" : "explicit"
|
|
4312
|
+
});
|
|
3792
4313
|
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3793
4314
|
capabilityRuntimeSettings,
|
|
3794
4315
|
existingSession.mcpServers
|
|
3795
4316
|
);
|
|
3796
4317
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
3797
|
-
const
|
|
3798
|
-
const
|
|
4318
|
+
const tracksWorkspaceDefaults = existingSession.toolPolicy?.mode === "workspace_default";
|
|
4319
|
+
const sessionPolicyTools = withFirstPartyTools(
|
|
4320
|
+
tracksWorkspaceDefaults ? withDefaultEnabledCapabilityMcpTools(
|
|
4321
|
+
availableToolRefs(existingSession.tools, runtimeSettings),
|
|
4322
|
+
settings,
|
|
4323
|
+
capabilityRuntimeSettings
|
|
4324
|
+
) : existingSession.tools,
|
|
4325
|
+
runtimeSettings
|
|
4326
|
+
);
|
|
4327
|
+
const validatedTools = input.toolsProvided ? validateToolRefsForSessionPolicy({
|
|
4328
|
+
requested: input.tools ?? [],
|
|
4329
|
+
settings: runtimeSettings,
|
|
4330
|
+
allowedTools: sessionPolicyTools,
|
|
4331
|
+
message: "message tools may only narrow the session tool policy"
|
|
4332
|
+
}) : [];
|
|
4333
|
+
const requestedTools = input.toolsProvided ? validatedTools : [];
|
|
3799
4334
|
await requireLimit(deps, {
|
|
3800
4335
|
accountId: grant.accountId,
|
|
3801
4336
|
workspaceId,
|
|
3802
4337
|
action: "agent_run:create",
|
|
3803
4338
|
quantity: 1,
|
|
3804
|
-
model:
|
|
4339
|
+
model: effectiveModel
|
|
3805
4340
|
});
|
|
3806
4341
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
3807
4342
|
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
@@ -3830,8 +4365,11 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
3830
4365
|
turnInstructions: input.turnInstructions ?? null,
|
|
3831
4366
|
resources: requestedResources,
|
|
3832
4367
|
tools: requestedTools,
|
|
4368
|
+
toolsProvided: input.toolsProvided,
|
|
3833
4369
|
model: input.model ?? null,
|
|
3834
4370
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
4371
|
+
reasoningEffortFallback: sessionReasoningEffort,
|
|
4372
|
+
turnExecutionPolicy,
|
|
3835
4373
|
mcpCredentialUpdates,
|
|
3836
4374
|
delivery: input.delivery ?? "send",
|
|
3837
4375
|
origin: delegatedServiceInitiator ? "operator" : input.origin ?? "human",
|
|
@@ -3877,7 +4415,7 @@ async function updateSessionTitle(deps, grant, sessionId, title, source) {
|
|
|
3877
4415
|
const workspaceId = grant.workspaceId;
|
|
3878
4416
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
3879
4417
|
if (result.updated) {
|
|
3880
|
-
await
|
|
4418
|
+
await appendAndPublishEvents2(db, bus, workspaceId, sessionId, [
|
|
3881
4419
|
{
|
|
3882
4420
|
type: "session.title_set",
|
|
3883
4421
|
payload: {
|
|
@@ -3892,6 +4430,48 @@ async function updateSessionTitle(deps, grant, sessionId, title, source) {
|
|
|
3892
4430
|
relatedSessionAccess: authorization?.relatedSessionAccess ?? "root"
|
|
3893
4431
|
};
|
|
3894
4432
|
}
|
|
4433
|
+
async function updateSessionMcpApprovalPolicy(deps, grant, sessionId, serverId, requireApproval) {
|
|
4434
|
+
const normalizedPolicy = SessionMcpApprovalPolicy.parse(requireApproval);
|
|
4435
|
+
await requireSessionAuthorization(deps, grant, {
|
|
4436
|
+
sessionId,
|
|
4437
|
+
operation: "session.mcp.approval_policy.write",
|
|
4438
|
+
surface: "core"
|
|
4439
|
+
});
|
|
4440
|
+
requirePermission(grant, "sessions:control");
|
|
4441
|
+
const outcome = {};
|
|
4442
|
+
const events = await appendSessionEventsWithLockedSessionUpdate(
|
|
4443
|
+
deps.db,
|
|
4444
|
+
grant.workspaceId,
|
|
4445
|
+
sessionId,
|
|
4446
|
+
async (_session, context) => {
|
|
4447
|
+
const result = await context.updateSessionMcpApprovalPolicy(serverId, normalizedPolicy);
|
|
4448
|
+
if (!result.server) {
|
|
4449
|
+
throw new HTTPException9(404, { message: "session MCP server not found" });
|
|
4450
|
+
}
|
|
4451
|
+
outcome.server = result.server;
|
|
4452
|
+
return {
|
|
4453
|
+
events: result.changed ? [
|
|
4454
|
+
{
|
|
4455
|
+
type: "session.mcp.approval_policy.updated",
|
|
4456
|
+
payload: {
|
|
4457
|
+
serverId,
|
|
4458
|
+
effectiveFrom: "next_attempt"
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
] : []
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
);
|
|
4465
|
+
const updatedServer = outcome.server;
|
|
4466
|
+
if (!updatedServer) {
|
|
4467
|
+
throw new Error("session MCP approval policy update returned no server");
|
|
4468
|
+
}
|
|
4469
|
+
await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
|
|
4470
|
+
return {
|
|
4471
|
+
server: updatedServer,
|
|
4472
|
+
effectiveFrom: "next_attempt"
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
3895
4475
|
async function readSessionLineage(deps, grant, sessionId) {
|
|
3896
4476
|
const authorization = await requireSessionAuthorization(deps, grant, {
|
|
3897
4477
|
sessionId,
|
|
@@ -4032,6 +4612,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
4032
4612
|
settings: input.settings,
|
|
4033
4613
|
db: input.db,
|
|
4034
4614
|
objectStorage: input.objectStorage,
|
|
4615
|
+
grant: input.grant,
|
|
4035
4616
|
workspaceId: input.existing.workspaceId,
|
|
4036
4617
|
payload: { agentConfig: input.payload.agentConfig },
|
|
4037
4618
|
...input.toolsProvided !== void 0 ? { toolsProvided: input.toolsProvided } : {}
|
|
@@ -4095,13 +4676,8 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
|
|
|
4095
4676
|
return `agent_run.created:scheduled-trigger:${workspaceId}:${taskId}:${triggerToken}`;
|
|
4096
4677
|
}
|
|
4097
4678
|
async function validateScheduledTaskAgentConfig(input) {
|
|
4098
|
-
|
|
4099
|
-
await assertWorkspaceModelPolicyAllows(
|
|
4100
|
-
input.db,
|
|
4101
|
-
input.settings,
|
|
4102
|
-
input.workspaceId,
|
|
4103
|
-
input.payload.agentConfig.model
|
|
4104
|
-
);
|
|
4679
|
+
const model = canonicalConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
4680
|
+
await assertWorkspaceModelPolicyAllows(input.db, input.settings, input.workspaceId, model);
|
|
4105
4681
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
4106
4682
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
4107
4683
|
input.db,
|
|
@@ -4119,8 +4695,21 @@ async function validateScheduledTaskAgentConfig(input) {
|
|
|
4119
4695
|
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
4120
4696
|
}
|
|
4121
4697
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
4698
|
+
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
4699
|
+
if (requestedMaxDepth !== void 0) {
|
|
4700
|
+
const workspace = await requireWorkspace2(input.db, input.workspaceId);
|
|
4701
|
+
const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
|
|
4702
|
+
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
4703
|
+
const inheritedMaxDepth = typeof workspaceMaxDepth === "number" ? workspaceMaxDepth : deploymentPolicy.maxNestedAgentDepth;
|
|
4704
|
+
if (requestedMaxDepth > inheritedMaxDepth && !hasPermission(input.grant.permissions, "workspace:admin")) {
|
|
4705
|
+
throw new HTTPException10(403, {
|
|
4706
|
+
message: `scheduled task maxNestedAgentDepth ${requestedMaxDepth} exceeds inherited limit ${inheritedMaxDepth}; workspace:admin is required to increase it`
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4122
4710
|
return {
|
|
4123
4711
|
...input.payload.agentConfig,
|
|
4712
|
+
...model === void 0 || model === null ? {} : { model },
|
|
4124
4713
|
prompt,
|
|
4125
4714
|
resources,
|
|
4126
4715
|
tools
|
|
@@ -4188,6 +4777,102 @@ function assertWorkspaceDeletable(input) {
|
|
|
4188
4777
|
}
|
|
4189
4778
|
}
|
|
4190
4779
|
|
|
4780
|
+
// src/application/new-session-drafts.ts
|
|
4781
|
+
import {
|
|
4782
|
+
NewSessionDraft,
|
|
4783
|
+
SaveNewSessionDraftRequest
|
|
4784
|
+
} from "@opengeni/contracts";
|
|
4785
|
+
import {
|
|
4786
|
+
getNewSessionDraftInTransaction,
|
|
4787
|
+
NewSessionDraftAccessError,
|
|
4788
|
+
saveNewSessionDraftInTransaction,
|
|
4789
|
+
withWorkspaceSubjectRls as withWorkspaceSubjectRls2
|
|
4790
|
+
} from "@opengeni/db";
|
|
4791
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
4792
|
+
function mapNewSessionDraft(row) {
|
|
4793
|
+
if (!row) return null;
|
|
4794
|
+
return NewSessionDraft.parse({
|
|
4795
|
+
revision: row.revision,
|
|
4796
|
+
text: row.text,
|
|
4797
|
+
resources: row.resources,
|
|
4798
|
+
tools: row.tools,
|
|
4799
|
+
model: row.model,
|
|
4800
|
+
reasoningEffort: row.reasoningEffort,
|
|
4801
|
+
options: row.sessionOptions,
|
|
4802
|
+
updatedAt: row.updatedAt.toISOString()
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
async function getActorNewSessionDraft(deps, grant, workspaceId) {
|
|
4806
|
+
const row = await withWorkspaceSubjectRls2(
|
|
4807
|
+
deps.db,
|
|
4808
|
+
workspaceId,
|
|
4809
|
+
grant.subjectId,
|
|
4810
|
+
(scoped) => getNewSessionDraftInTransaction(scoped, {
|
|
4811
|
+
workspaceId,
|
|
4812
|
+
subjectId: grant.subjectId
|
|
4813
|
+
})
|
|
4814
|
+
);
|
|
4815
|
+
return mapNewSessionDraft(row) ?? {
|
|
4816
|
+
revision: 0,
|
|
4817
|
+
text: "",
|
|
4818
|
+
resources: [],
|
|
4819
|
+
tools: [],
|
|
4820
|
+
model: deps.settings.openaiModel,
|
|
4821
|
+
reasoningEffort: deps.settings.openaiReasoningEffort,
|
|
4822
|
+
options: {},
|
|
4823
|
+
updatedAt: null
|
|
4824
|
+
};
|
|
4825
|
+
}
|
|
4826
|
+
async function saveActorNewSessionDraft(deps, grant, workspaceId, rawInput) {
|
|
4827
|
+
const input = SaveNewSessionDraftRequest.parse(rawInput);
|
|
4828
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
4829
|
+
deps.db,
|
|
4830
|
+
workspaceId,
|
|
4831
|
+
deps.settings
|
|
4832
|
+
);
|
|
4833
|
+
const resources = normalizeResources(input.resources);
|
|
4834
|
+
const tools = validateToolRefs(input.tools, runtimeSettings);
|
|
4835
|
+
await validateGitHubRepositorySelection(deps.db, workspaceId, resources);
|
|
4836
|
+
if (resources.some((resource) => resource.kind === "file") && !deps.objectStorage) {
|
|
4837
|
+
throw new HTTPException12(503, { message: "object storage is not configured" });
|
|
4838
|
+
}
|
|
4839
|
+
await validateFileResources(deps.db, workspaceId, resources);
|
|
4840
|
+
assertConfiguredModel(deps.settings, input.model);
|
|
4841
|
+
await assertWorkspaceModelPolicyAllows(deps.db, deps.settings, workspaceId, input.model);
|
|
4842
|
+
try {
|
|
4843
|
+
const saved = await withWorkspaceSubjectRls2(
|
|
4844
|
+
deps.db,
|
|
4845
|
+
workspaceId,
|
|
4846
|
+
grant.subjectId,
|
|
4847
|
+
(scoped) => scoped.transaction(
|
|
4848
|
+
(tx) => saveNewSessionDraftInTransaction(tx, {
|
|
4849
|
+
accountId: grant.accountId,
|
|
4850
|
+
workspaceId,
|
|
4851
|
+
subjectId: grant.subjectId,
|
|
4852
|
+
expectedRevision: input.expectedRevision,
|
|
4853
|
+
text: input.text,
|
|
4854
|
+
resources,
|
|
4855
|
+
tools,
|
|
4856
|
+
model: input.model,
|
|
4857
|
+
reasoningEffort: input.reasoningEffort,
|
|
4858
|
+
options: input.options,
|
|
4859
|
+
// Only managed people are removed through removeWorkspaceMember().
|
|
4860
|
+
// API keys and delegated service actors (for example the first-party
|
|
4861
|
+
// worker MCP principal) legitimately have no workspace_memberships
|
|
4862
|
+
// row, so they must not be rejected by the human-removal fence.
|
|
4863
|
+
requireWorkspaceMembership: grant.subjectId.startsWith("user:")
|
|
4864
|
+
})
|
|
4865
|
+
)
|
|
4866
|
+
);
|
|
4867
|
+
return mapNewSessionDraft(saved);
|
|
4868
|
+
} catch (error) {
|
|
4869
|
+
if (error instanceof NewSessionDraftAccessError) {
|
|
4870
|
+
throw new HTTPException12(403, { message: error.message });
|
|
4871
|
+
}
|
|
4872
|
+
throw error;
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4191
4876
|
// src/application/session-commands.ts
|
|
4192
4877
|
import { reasoningEffortForMetadata as reasoningEffortForMetadata2 } from "@opengeni/contracts";
|
|
4193
4878
|
import {
|
|
@@ -4209,7 +4894,7 @@ import {
|
|
|
4209
4894
|
steerAgentSessionInTransaction,
|
|
4210
4895
|
steerQueuedTurnInTransaction,
|
|
4211
4896
|
withWorkspaceRls,
|
|
4212
|
-
withWorkspaceSubjectRls as
|
|
4897
|
+
withWorkspaceSubjectRls as withWorkspaceSubjectRls3
|
|
4213
4898
|
} from "@opengeni/db";
|
|
4214
4899
|
import {
|
|
4215
4900
|
publishDurableSessionEvents as publishDurableSessionEvents2,
|
|
@@ -4423,6 +5108,7 @@ function composerDraft(row) {
|
|
|
4423
5108
|
text: row.text,
|
|
4424
5109
|
resources: row.resources,
|
|
4425
5110
|
tools: row.tools,
|
|
5111
|
+
toolsProvided: row.toolsProvided,
|
|
4426
5112
|
model: row.model,
|
|
4427
5113
|
reasoningEffort: row.reasoningEffort,
|
|
4428
5114
|
sourceTurnId: row.sourceTurnId,
|
|
@@ -4500,7 +5186,7 @@ async function deleteHumanQueuePrompt(deps, context, turnId, input) {
|
|
|
4500
5186
|
}
|
|
4501
5187
|
async function editHumanQueuePrompt(deps, context, turnId, input) {
|
|
4502
5188
|
const authorization = await authorizeHumanSessionCommand(deps, context, "session.queue.control");
|
|
4503
|
-
const result = await
|
|
5189
|
+
const result = await withWorkspaceSubjectRls3(
|
|
4504
5190
|
deps.db,
|
|
4505
5191
|
context.workspaceId,
|
|
4506
5192
|
context.subjectId,
|
|
@@ -4622,7 +5308,7 @@ async function controlHumanWorkspace(deps, context, input) {
|
|
|
4622
5308
|
}
|
|
4623
5309
|
async function getHumanComposerDraft(deps, context) {
|
|
4624
5310
|
await authorizeHumanSessionCommand(deps, context, "session.composer.read");
|
|
4625
|
-
const row = await
|
|
5311
|
+
const row = await withWorkspaceSubjectRls3(
|
|
4626
5312
|
deps.db,
|
|
4627
5313
|
context.workspaceId,
|
|
4628
5314
|
context.subjectId,
|
|
@@ -4641,6 +5327,7 @@ async function getHumanComposerDraft(deps, context) {
|
|
|
4641
5327
|
text: "",
|
|
4642
5328
|
resources: [],
|
|
4643
5329
|
tools: [],
|
|
5330
|
+
toolsProvided: false,
|
|
4644
5331
|
model: session.model,
|
|
4645
5332
|
reasoningEffort: reasoningEffortForMetadata2(session.metadata, "medium"),
|
|
4646
5333
|
sourceTurnId: null,
|
|
@@ -4650,7 +5337,7 @@ async function getHumanComposerDraft(deps, context) {
|
|
|
4650
5337
|
}
|
|
4651
5338
|
async function saveHumanComposerDraft(deps, context, input) {
|
|
4652
5339
|
await authorizeHumanSessionCommand(deps, context, "session.composer.write");
|
|
4653
|
-
const row = await
|
|
5340
|
+
const row = await withWorkspaceSubjectRls3(
|
|
4654
5341
|
deps.db,
|
|
4655
5342
|
context.workspaceId,
|
|
4656
5343
|
context.subjectId,
|
|
@@ -4678,6 +5365,7 @@ export {
|
|
|
4678
5365
|
SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
|
|
4679
5366
|
SessionAuthorizationDeniedError,
|
|
4680
5367
|
SessionAuthorizationUnavailableError,
|
|
5368
|
+
SessionSpawnDeniedError,
|
|
4681
5369
|
acceptSessionUserMessage,
|
|
4682
5370
|
activateRigVersionForApi,
|
|
4683
5371
|
appendRigSetupCommand,
|
|
@@ -4686,12 +5374,15 @@ export {
|
|
|
4686
5374
|
assertAllowedVariableSetVariableName,
|
|
4687
5375
|
assertConfiguredModel,
|
|
4688
5376
|
assertPackSandboxImageCompatible,
|
|
5377
|
+
assertToolRefsSubset,
|
|
4689
5378
|
assertWorkspaceDeletable,
|
|
4690
5379
|
assertWorkspaceMemberRemovable,
|
|
4691
5380
|
assertWorkspaceModelPolicyAllows,
|
|
5381
|
+
availableToolRefs,
|
|
4692
5382
|
buildCapabilityCatalog,
|
|
4693
5383
|
buildFleetContextForSession,
|
|
4694
5384
|
buildMarketingDailyAnalysisAgentConfig,
|
|
5385
|
+
canonicalConfiguredModel,
|
|
4695
5386
|
checkLimit,
|
|
4696
5387
|
classifyRigVerificationOutcome,
|
|
4697
5388
|
controlAgentSessionWorkstream,
|
|
@@ -4710,6 +5401,7 @@ export {
|
|
|
4710
5401
|
editHumanQueuePrompt,
|
|
4711
5402
|
enableCapability,
|
|
4712
5403
|
enabledCapabilityMcpToolRefs,
|
|
5404
|
+
getActorNewSessionDraft,
|
|
4713
5405
|
getCapabilityPack,
|
|
4714
5406
|
getHumanComposerDraft,
|
|
4715
5407
|
hasPermission,
|
|
@@ -4755,15 +5447,19 @@ export {
|
|
|
4755
5447
|
requireVariableSetForApi,
|
|
4756
5448
|
resolveCapabilityPack,
|
|
4757
5449
|
resolveMemberSubjectId,
|
|
5450
|
+
resolveSessionToolPolicy,
|
|
4758
5451
|
restoreScheduledTask,
|
|
4759
5452
|
rigActorForGrant,
|
|
4760
5453
|
routingEnabled,
|
|
4761
5454
|
runOnSandbox,
|
|
5455
|
+
saveActorNewSessionDraft,
|
|
4762
5456
|
saveHumanComposerDraft,
|
|
4763
5457
|
scheduledTaskTemporalScheduleId,
|
|
4764
5458
|
scheduledTaskToolsProvided,
|
|
4765
5459
|
scheduledTaskTriggerToken,
|
|
4766
5460
|
sendAgentSessionMessage,
|
|
5461
|
+
sessionSpawnDenialEnvelope,
|
|
5462
|
+
sessionWithEffectiveToolPolicy,
|
|
4767
5463
|
settingsWithEnabledCapabilityMcpServers,
|
|
4768
5464
|
settingsWithMcpCapabilityServers,
|
|
4769
5465
|
settingsWithSessionMcpServerMetadata,
|
|
@@ -4774,6 +5470,7 @@ export {
|
|
|
4774
5470
|
syncCreatedScheduledTask,
|
|
4775
5471
|
syncUpdatedScheduledTask,
|
|
4776
5472
|
updateRigForApi,
|
|
5473
|
+
updateSessionMcpApprovalPolicy,
|
|
4777
5474
|
updateSessionTitle,
|
|
4778
5475
|
validateFileResources,
|
|
4779
5476
|
validateGitHubRepositorySelection,
|
|
@@ -4781,10 +5478,13 @@ export {
|
|
|
4781
5478
|
validateGitHubRepositorySelectionShapes,
|
|
4782
5479
|
validateMcpCapabilityConnection,
|
|
4783
5480
|
validateToolRefs,
|
|
5481
|
+
validateToolRefsForSessionPolicy,
|
|
4784
5482
|
validateVariableSetAttachment,
|
|
4785
5483
|
validatedScheduledTaskUpdate,
|
|
4786
5484
|
withDefaultEnabledCapabilityMcpTools,
|
|
4787
5485
|
workflowIdForSession,
|
|
5486
|
+
workspaceSessionToolPolicyDefaultServerIds,
|
|
5487
|
+
workspaceSessionToolPolicyServerIds,
|
|
4788
5488
|
wrapChannelABoxWithRouting
|
|
4789
5489
|
};
|
|
4790
5490
|
//# sourceMappingURL=index.js.map
|