@opengeni/core 0.4.6 → 0.4.8
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 +293 -32
- package/dist/index.js +1750 -472
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
- package/src/access/index.ts +136 -57
- package/src/application/session-commands.ts +527 -0
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +67 -14
- package/src/domain/capabilities.ts +380 -181
- package/src/domain/environments.ts +58 -38
- package/src/domain/packs.ts +49 -16
- package/src/domain/resources.ts +71 -28
- package/src/domain/scheduled-tasks.ts +107 -41
- package/src/domain/sessions.ts +531 -255
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +3 -0
- package/src/rigs/index.ts +540 -0
- package/src/sandbox/fleet.ts +97 -18
- package/src/sandbox/routing.ts +6 -1
- package/src/sandbox-types.ts +17 -5
- package/src/workflow-wake-contract.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// src/workflow-wake-contract.ts
|
|
2
|
+
var SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID = "opengeni-session-workflow-wake-dispatcher";
|
|
3
|
+
var SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE = "sessionWorkflowWakeDispatcherWorkflow";
|
|
4
|
+
var SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS = 1e4;
|
|
5
|
+
|
|
1
6
|
// src/sandbox/fleet.ts
|
|
2
7
|
import {
|
|
3
8
|
getEnrollment,
|
|
@@ -10,7 +15,8 @@ import {
|
|
|
10
15
|
import {
|
|
11
16
|
NatsControlRpc as NatsControlRpc2,
|
|
12
17
|
selfhostedLiveness,
|
|
13
|
-
SelfhostedSession
|
|
18
|
+
SelfhostedSession,
|
|
19
|
+
swapTargetEstablishability
|
|
14
20
|
} from "@opengeni/runtime/sandbox";
|
|
15
21
|
import { HTTPException } from "hono/http-exception";
|
|
16
22
|
|
|
@@ -63,7 +69,12 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
63
69
|
defaultKind: established.backendId,
|
|
64
70
|
getSandbox: async (sandboxId) => {
|
|
65
71
|
const sandbox = await getSandbox(db, ids.workspaceId, sandboxId);
|
|
66
|
-
return sandbox ? {
|
|
72
|
+
return sandbox ? {
|
|
73
|
+
id: sandbox.id,
|
|
74
|
+
kind: sandbox.kind,
|
|
75
|
+
name: sandbox.name,
|
|
76
|
+
enrollmentId: sandbox.enrollmentId
|
|
77
|
+
} : null;
|
|
67
78
|
},
|
|
68
79
|
controlRpcFactory: controlRpcFactory(bus),
|
|
69
80
|
relay: relayConfigFromSettings(settings)
|
|
@@ -126,7 +137,9 @@ async function probeEnrollment(services, workspaceId, enrollment) {
|
|
|
126
137
|
exposure: enrollment.exposure,
|
|
127
138
|
allowScreenControl: enrollment.allowScreenControl,
|
|
128
139
|
hasDisplay: enrollment.hasDisplay,
|
|
129
|
-
lastSeenAt: enrollment.lastSeenAt
|
|
140
|
+
lastSeenAt: enrollment.lastSeenAt,
|
|
141
|
+
wentOfflineAt: enrollment.wentOfflineAt,
|
|
142
|
+
wentOfflineReason: enrollment.wentOfflineReason
|
|
130
143
|
},
|
|
131
144
|
probeResponded
|
|
132
145
|
});
|
|
@@ -171,7 +184,11 @@ async function listFleet(services, ctx) {
|
|
|
171
184
|
lastSeenAt: enrollment?.lastSeenAt ?? null
|
|
172
185
|
});
|
|
173
186
|
}
|
|
174
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
189
|
+
activeEpoch: pointer.activeEpoch,
|
|
190
|
+
sandboxes: entries
|
|
191
|
+
};
|
|
175
192
|
}
|
|
176
193
|
async function resolveTarget(services, ctx, target) {
|
|
177
194
|
if (target === ctx.sessionGroupId || target === "session" || target === "default") {
|
|
@@ -179,19 +196,42 @@ async function resolveTarget(services, ctx, target) {
|
|
|
179
196
|
}
|
|
180
197
|
const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
|
|
181
198
|
if (!sandbox) {
|
|
182
|
-
return {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
reason: `sandbox ${target} not found in this workspace`,
|
|
202
|
+
code: "stale_pointer"
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const establishable = swapTargetEstablishability({
|
|
206
|
+
kind: sandbox.kind,
|
|
207
|
+
isSessionGroup: false
|
|
208
|
+
});
|
|
209
|
+
if (!establishable.ok) {
|
|
210
|
+
return { ok: false, reason: establishable.reason, code: establishable.code };
|
|
183
211
|
}
|
|
184
212
|
if (sandbox.kind === "selfhosted") {
|
|
185
213
|
if (!sandbox.enrollmentId) {
|
|
186
|
-
return {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
reason: `selfhosted sandbox ${target} has no enrollment`,
|
|
217
|
+
code: "offline_enrollment"
|
|
218
|
+
};
|
|
187
219
|
}
|
|
188
220
|
const enrollment = await getEnrollment(services.db, ctx.workspaceId, sandbox.enrollmentId);
|
|
189
221
|
if (!enrollment) {
|
|
190
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
reason: `enrollment for sandbox ${target} not found`,
|
|
225
|
+
code: "offline_enrollment"
|
|
226
|
+
};
|
|
191
227
|
}
|
|
192
228
|
const probe = await probeEnrollment(services, ctx.workspaceId, enrollment);
|
|
193
229
|
if (probe.liveness !== "online") {
|
|
194
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: `sandbox ${target} is ${probe.liveness}; cannot attach to a non-online machine`,
|
|
233
|
+
code: "offline_enrollment"
|
|
234
|
+
};
|
|
195
235
|
}
|
|
196
236
|
}
|
|
197
237
|
return { ok: true, targetSandboxId: sandbox.id };
|
|
@@ -203,7 +243,13 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
203
243
|
activeSandboxId: null,
|
|
204
244
|
activeEpoch: 0
|
|
205
245
|
};
|
|
206
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
swapped: false,
|
|
248
|
+
activeSandboxId: pointer2.activeSandboxId,
|
|
249
|
+
activeEpoch: pointer2.activeEpoch,
|
|
250
|
+
reason: resolved.reason,
|
|
251
|
+
code: resolved.code
|
|
252
|
+
};
|
|
207
253
|
}
|
|
208
254
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
209
255
|
const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
@@ -211,7 +257,11 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
211
257
|
activeEpoch: 0
|
|
212
258
|
};
|
|
213
259
|
if (pointer2.activeSandboxId === resolved.targetSandboxId) {
|
|
214
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
swapped: true,
|
|
262
|
+
activeSandboxId: pointer2.activeSandboxId,
|
|
263
|
+
activeEpoch: pointer2.activeEpoch
|
|
264
|
+
};
|
|
215
265
|
}
|
|
216
266
|
const result = await setActiveSandbox(services.db, {
|
|
217
267
|
accountId: ctx.accountId,
|
|
@@ -222,7 +272,11 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
222
272
|
...workingDir !== void 0 ? { workingDir } : {}
|
|
223
273
|
});
|
|
224
274
|
if (result.swapped && result.pointer) {
|
|
225
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
swapped: true,
|
|
277
|
+
activeSandboxId: result.pointer.activeSandboxId,
|
|
278
|
+
activeEpoch: result.pointer.activeEpoch
|
|
279
|
+
};
|
|
226
280
|
}
|
|
227
281
|
}
|
|
228
282
|
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
@@ -233,13 +287,19 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
233
287
|
swapped: false,
|
|
234
288
|
activeSandboxId: pointer.activeSandboxId,
|
|
235
289
|
activeEpoch: pointer.activeEpoch,
|
|
236
|
-
reason: "a concurrent swap won the epoch fence; re-read and retry"
|
|
290
|
+
reason: "a concurrent swap won the epoch fence; re-read and retry",
|
|
291
|
+
code: "concurrent_swap"
|
|
237
292
|
};
|
|
238
293
|
}
|
|
239
294
|
async function runOnSandbox(services, ctx, target, op) {
|
|
240
295
|
const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
|
|
241
296
|
if (!sandbox) {
|
|
242
|
-
return {
|
|
297
|
+
return {
|
|
298
|
+
target,
|
|
299
|
+
kind: op.kind,
|
|
300
|
+
ok: false,
|
|
301
|
+
reason: `sandbox ${target} not found in this workspace`
|
|
302
|
+
};
|
|
243
303
|
}
|
|
244
304
|
if (sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
245
305
|
return {
|
|
@@ -261,8 +321,18 @@ async function runOnSandbox(services, ctx, target, op) {
|
|
|
261
321
|
});
|
|
262
322
|
try {
|
|
263
323
|
if (op.kind === "exec") {
|
|
264
|
-
const res = await session.exec({
|
|
265
|
-
|
|
324
|
+
const res = await session.exec({
|
|
325
|
+
cmd: op.cmd,
|
|
326
|
+
...op.workdir ? { workdir: op.workdir } : {}
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
target,
|
|
330
|
+
kind: "exec",
|
|
331
|
+
ok: true,
|
|
332
|
+
stdout: res.stdout,
|
|
333
|
+
stderr: res.stderr,
|
|
334
|
+
exitCode: res.exitCode
|
|
335
|
+
};
|
|
266
336
|
}
|
|
267
337
|
if (op.kind === "read") {
|
|
268
338
|
const bytes = await session.readFile({ path: op.path });
|
|
@@ -301,12 +371,14 @@ async function provisionSandbox(services, ctx, input) {
|
|
|
301
371
|
return {
|
|
302
372
|
kind: "modal",
|
|
303
373
|
sandbox,
|
|
304
|
-
note: "A named Modal sandbox record was created
|
|
374
|
+
note: "A named Modal sandbox record was created, but it is NOT yet attachable as a swap target: routing a session onto a second Modal box is not supported yet, so a sandbox_swap to this id is rejected. Use the session's own box (the default) or attach a Connected Machine instead."
|
|
305
375
|
};
|
|
306
376
|
}
|
|
307
377
|
|
|
308
378
|
// src/access/index.ts
|
|
309
|
-
import {
|
|
379
|
+
import {
|
|
380
|
+
verifyDelegatedAccessToken
|
|
381
|
+
} from "@opengeni/contracts";
|
|
310
382
|
import {
|
|
311
383
|
bootstrapWorkspace,
|
|
312
384
|
ensureManagedAccessForUser,
|
|
@@ -340,11 +412,25 @@ async function requireAccessGrant(c, deps, workspaceId, permission) {
|
|
|
340
412
|
}
|
|
341
413
|
function requirePermission(grant, permission) {
|
|
342
414
|
if (!hasPermission(grant.permissions, permission)) {
|
|
415
|
+
if (permission === "variable-sets:use") {
|
|
416
|
+
throw new HTTPException2(403, {
|
|
417
|
+
message: "missing permission: variable-sets:use (deprecated alias: environments:use)"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (permission === "variable-sets:manage") {
|
|
421
|
+
throw new HTTPException2(403, {
|
|
422
|
+
message: "missing permission: variable-sets:manage (deprecated alias: environments:manage)"
|
|
423
|
+
});
|
|
424
|
+
}
|
|
343
425
|
throw new HTTPException2(403, { message: `missing permission: ${permission}` });
|
|
344
426
|
}
|
|
345
427
|
}
|
|
346
428
|
function hasPermission(permissions, permission) {
|
|
347
|
-
|
|
429
|
+
const aliases = {
|
|
430
|
+
"variable-sets:use": ["environments:use"],
|
|
431
|
+
"variable-sets:manage": ["environments:manage"]
|
|
432
|
+
};
|
|
433
|
+
return permissions.includes(permission) || (aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) || permissions.includes("workspace:admin");
|
|
348
434
|
}
|
|
349
435
|
async function resolveAccessContext(c, deps) {
|
|
350
436
|
if (deps.settings.productAccessMode === "local") {
|
|
@@ -364,6 +450,10 @@ async function resolveAccessContext(c, deps) {
|
|
|
364
450
|
if (delegated) {
|
|
365
451
|
return delegated;
|
|
366
452
|
}
|
|
453
|
+
const apiKey = await apiKeyAccessContext(c, deps, "configured");
|
|
454
|
+
if (apiKey) {
|
|
455
|
+
return apiKey;
|
|
456
|
+
}
|
|
367
457
|
if (deps.settings.delegationSecret) {
|
|
368
458
|
return null;
|
|
369
459
|
}
|
|
@@ -384,29 +474,9 @@ async function resolveAccessContext(c, deps) {
|
|
|
384
474
|
if (delegated) {
|
|
385
475
|
return delegated;
|
|
386
476
|
}
|
|
387
|
-
const apiKey = await
|
|
477
|
+
const apiKey = await apiKeyAccessContext(c, deps, "managed");
|
|
388
478
|
if (apiKey) {
|
|
389
|
-
|
|
390
|
-
return {
|
|
391
|
-
mode: "managed",
|
|
392
|
-
subjectId: `api_key:${apiKey.id}`,
|
|
393
|
-
subjectLabel: apiKey.name,
|
|
394
|
-
accountGrants: [{
|
|
395
|
-
accountId: apiKey.accountId,
|
|
396
|
-
subjectId: `api_key:${apiKey.id}`,
|
|
397
|
-
subjectLabel: apiKey.name,
|
|
398
|
-
permissions: accountPermissions
|
|
399
|
-
}],
|
|
400
|
-
workspaceGrants: apiKey.workspaceId ? [{
|
|
401
|
-
workspaceId: apiKey.workspaceId,
|
|
402
|
-
accountId: apiKey.accountId,
|
|
403
|
-
subjectId: `api_key:${apiKey.id}`,
|
|
404
|
-
subjectLabel: apiKey.name,
|
|
405
|
-
permissions: apiKey.permissions
|
|
406
|
-
}] : [],
|
|
407
|
-
defaultAccountId: apiKey.accountId,
|
|
408
|
-
defaultWorkspaceId: apiKey.workspaceId
|
|
409
|
-
};
|
|
479
|
+
return apiKey;
|
|
410
480
|
}
|
|
411
481
|
}
|
|
412
482
|
if (deps.managedAuth) {
|
|
@@ -421,6 +491,44 @@ async function resolveAccessContext(c, deps) {
|
|
|
421
491
|
}
|
|
422
492
|
return null;
|
|
423
493
|
}
|
|
494
|
+
async function apiKeyAccessContext(c, deps, mode) {
|
|
495
|
+
const bearer = bearerToken(c);
|
|
496
|
+
if (!bearer) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const apiKey = await findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
|
|
500
|
+
if (!apiKey) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const subjectId = `api_key:${apiKey.id}`;
|
|
504
|
+
const accountPermissions = apiKey.workspaceId ? apiKey.permissions.filter(
|
|
505
|
+
(permission) => permission === "billing:read" || permission === "billing:manage"
|
|
506
|
+
) : apiKey.permissions;
|
|
507
|
+
return {
|
|
508
|
+
mode,
|
|
509
|
+
subjectId,
|
|
510
|
+
subjectLabel: apiKey.name,
|
|
511
|
+
accountGrants: [
|
|
512
|
+
{
|
|
513
|
+
accountId: apiKey.accountId,
|
|
514
|
+
subjectId,
|
|
515
|
+
subjectLabel: apiKey.name,
|
|
516
|
+
permissions: accountPermissions
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
workspaceGrants: apiKey.workspaceId ? [
|
|
520
|
+
{
|
|
521
|
+
workspaceId: apiKey.workspaceId,
|
|
522
|
+
accountId: apiKey.accountId,
|
|
523
|
+
subjectId,
|
|
524
|
+
subjectLabel: apiKey.name,
|
|
525
|
+
permissions: apiKey.permissions
|
|
526
|
+
}
|
|
527
|
+
] : [],
|
|
528
|
+
defaultAccountId: apiKey.accountId,
|
|
529
|
+
defaultWorkspaceId: apiKey.workspaceId
|
|
530
|
+
};
|
|
531
|
+
}
|
|
424
532
|
async function delegatedAccessContext(c, deps, mode, token = bearerToken(c)) {
|
|
425
533
|
if (!token || !deps.settings.delegationSecret) {
|
|
426
534
|
return null;
|
|
@@ -433,22 +541,34 @@ async function delegatedAccessContext(c, deps, mode, token = bearerToken(c)) {
|
|
|
433
541
|
mode,
|
|
434
542
|
subjectId: payload.subjectId,
|
|
435
543
|
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
436
|
-
accountGrants: [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
544
|
+
accountGrants: [
|
|
545
|
+
{
|
|
546
|
+
accountId: payload.accountId,
|
|
547
|
+
subjectId: payload.subjectId,
|
|
548
|
+
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
549
|
+
permissions: payload.permissions
|
|
550
|
+
}
|
|
551
|
+
],
|
|
552
|
+
workspaceGrants: [
|
|
553
|
+
{
|
|
554
|
+
workspaceId: payload.workspaceId,
|
|
555
|
+
accountId: payload.accountId,
|
|
556
|
+
subjectId: payload.subjectId,
|
|
557
|
+
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
558
|
+
permissions: payload.permissions,
|
|
559
|
+
// sessionId is worker-asserted (HMAC-signed token claim), not agent
|
|
560
|
+
// controlled; it scopes session-bound MCP tools such as goal management.
|
|
561
|
+
metadata: {
|
|
562
|
+
delegated: true,
|
|
563
|
+
...payload.sessionId ? { sessionId: payload.sessionId } : {},
|
|
564
|
+
// Caller identity: the turn that minted this token. Tools classify the
|
|
565
|
+
// CALLER from this instead of re-reading the live active pointer.
|
|
566
|
+
...payload.turnId ? { turnId: payload.turnId } : {},
|
|
567
|
+
...payload.attemptId ? { attemptId: payload.attemptId } : {},
|
|
568
|
+
...payload.executionGeneration ? { executionGeneration: payload.executionGeneration } : {}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
],
|
|
452
572
|
defaultAccountId: payload.accountId,
|
|
453
573
|
defaultWorkspaceId: payload.workspaceId
|
|
454
574
|
};
|
|
@@ -483,10 +603,17 @@ async function requireLimit(deps, input) {
|
|
|
483
603
|
if (decision.allowed) {
|
|
484
604
|
return;
|
|
485
605
|
}
|
|
486
|
-
throw new HTTPException3(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
606
|
+
throw new HTTPException3(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
607
|
+
message: decision.message
|
|
608
|
+
});
|
|
487
609
|
}
|
|
488
610
|
async function checkLimit(deps, input) {
|
|
489
|
-
const codexBilled = input.workspaceId ? await isCodexBilledTurn({
|
|
611
|
+
const codexBilled = input.workspaceId ? await isCodexBilledTurn({
|
|
612
|
+
db: deps.db,
|
|
613
|
+
settings: deps.settings,
|
|
614
|
+
workspaceId: input.workspaceId,
|
|
615
|
+
model: input.model
|
|
616
|
+
}) : false;
|
|
490
617
|
const creditDecision = await checkCreditBalance(deps, input, codexBilled);
|
|
491
618
|
if (!creditDecision.allowed) {
|
|
492
619
|
return creditDecision;
|
|
@@ -518,7 +645,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
518
645
|
since: startOfUtcMonth()
|
|
519
646
|
});
|
|
520
647
|
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
521
|
-
return blocked(
|
|
648
|
+
return blocked(
|
|
649
|
+
"max_monthly_cost_micros_per_account",
|
|
650
|
+
`monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`
|
|
651
|
+
);
|
|
522
652
|
}
|
|
523
653
|
}
|
|
524
654
|
switch (input.action) {
|
|
@@ -527,27 +657,39 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
527
657
|
return { allowed: true };
|
|
528
658
|
}
|
|
529
659
|
const count = await countWorkspacesForAccount(deps.db, input.accountId);
|
|
530
|
-
return count < limits.maxWorkspacesPerAccount ? { allowed: true } : blocked(
|
|
660
|
+
return count < limits.maxWorkspacesPerAccount ? { allowed: true } : blocked(
|
|
661
|
+
"max_workspaces_per_account",
|
|
662
|
+
`workspace limit reached (${limits.maxWorkspacesPerAccount})`
|
|
663
|
+
);
|
|
531
664
|
}
|
|
532
665
|
case "api_key:create": {
|
|
533
666
|
if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
|
|
534
667
|
return { allowed: true };
|
|
535
668
|
}
|
|
536
669
|
const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
|
|
537
|
-
return count < limits.maxApiKeysPerWorkspace ? { allowed: true } : blocked(
|
|
670
|
+
return count < limits.maxApiKeysPerWorkspace ? { allowed: true } : blocked(
|
|
671
|
+
"max_api_keys_per_workspace",
|
|
672
|
+
`API key limit reached (${limits.maxApiKeysPerWorkspace})`
|
|
673
|
+
);
|
|
538
674
|
}
|
|
539
675
|
case "schedule:create": {
|
|
540
676
|
if (!limits.maxSchedulesPerWorkspace || !input.workspaceId) {
|
|
541
677
|
return { allowed: true };
|
|
542
678
|
}
|
|
543
679
|
const count = await countScheduledTasksForWorkspace(deps.db, input.workspaceId);
|
|
544
|
-
return count < limits.maxSchedulesPerWorkspace ? { allowed: true } : blocked(
|
|
680
|
+
return count < limits.maxSchedulesPerWorkspace ? { allowed: true } : blocked(
|
|
681
|
+
"max_schedules_per_workspace",
|
|
682
|
+
`scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`
|
|
683
|
+
);
|
|
545
684
|
}
|
|
546
685
|
case "file:upload": {
|
|
547
686
|
if (!limits.maxFileUploadBytes || !input.quantity) {
|
|
548
687
|
return { allowed: true };
|
|
549
688
|
}
|
|
550
|
-
return input.quantity <= limits.maxFileUploadBytes ? { allowed: true } : blocked(
|
|
689
|
+
return input.quantity <= limits.maxFileUploadBytes ? { allowed: true } : blocked(
|
|
690
|
+
"max_file_upload_bytes",
|
|
691
|
+
`file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`
|
|
692
|
+
);
|
|
551
693
|
}
|
|
552
694
|
case "agent_run:create": {
|
|
553
695
|
if (!limits.maxMonthlyAgentRunsPerWorkspace || !input.workspaceId) {
|
|
@@ -559,7 +701,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
559
701
|
since: startOfUtcMonth()
|
|
560
702
|
});
|
|
561
703
|
const requested = input.quantity ?? 0;
|
|
562
|
-
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace ? { allowed: true } : blocked(
|
|
704
|
+
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace ? { allowed: true } : blocked(
|
|
705
|
+
"max_monthly_agent_runs_per_workspace",
|
|
706
|
+
`monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`
|
|
707
|
+
);
|
|
563
708
|
}
|
|
564
709
|
case "tokens:consume": {
|
|
565
710
|
if (codexBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
|
|
@@ -571,7 +716,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
571
716
|
since: startOfUtcMonth()
|
|
572
717
|
});
|
|
573
718
|
const requested = input.quantity ?? 0;
|
|
574
|
-
return used + requested <= limits.maxMonthlyTokensPerWorkspace ? { allowed: true } : blocked(
|
|
719
|
+
return used + requested <= limits.maxMonthlyTokensPerWorkspace ? { allowed: true } : blocked(
|
|
720
|
+
"max_monthly_tokens_per_workspace",
|
|
721
|
+
`monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`
|
|
722
|
+
);
|
|
575
723
|
}
|
|
576
724
|
case "document:index": {
|
|
577
725
|
if (!limits.maxDocumentIndexedChunksPerWorkspace || !input.workspaceId) {
|
|
@@ -583,7 +731,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
583
731
|
since: startOfUtcMonth()
|
|
584
732
|
});
|
|
585
733
|
const requested = input.quantity ?? 0;
|
|
586
|
-
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace ? { allowed: true } : blocked(
|
|
734
|
+
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace ? { allowed: true } : blocked(
|
|
735
|
+
"max_document_indexed_chunks_per_workspace",
|
|
736
|
+
`monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`
|
|
737
|
+
);
|
|
587
738
|
}
|
|
588
739
|
}
|
|
589
740
|
}
|
|
@@ -604,7 +755,7 @@ function usesCreditLimits(deps) {
|
|
|
604
755
|
return deps.settings.billingMode === "stripe" || deps.settings.usageLimitsMode === "managed";
|
|
605
756
|
}
|
|
606
757
|
function isCostlyAction(action) {
|
|
607
|
-
return action === "agent_run:create" || action === "tokens:consume" || action === "
|
|
758
|
+
return action === "agent_run:create" || action === "tokens:consume" || action === "document:index";
|
|
608
759
|
}
|
|
609
760
|
function blocked(code, message) {
|
|
610
761
|
return { allowed: false, code, message };
|
|
@@ -623,18 +774,18 @@ import {
|
|
|
623
774
|
CapabilityCatalogItem
|
|
624
775
|
} from "@opengeni/contracts";
|
|
625
776
|
import {
|
|
626
|
-
|
|
777
|
+
decryptVariableSetValue,
|
|
627
778
|
decryptedCapabilityHeaders,
|
|
628
779
|
disableCapabilityInstallation,
|
|
629
780
|
enableCapabilityInstallation,
|
|
630
781
|
enablePackInstallation,
|
|
631
|
-
|
|
782
|
+
encryptVariableSetValue,
|
|
632
783
|
getCapabilityCatalogItem,
|
|
633
784
|
getCapabilityInstallation,
|
|
634
785
|
getConnectionMetadata,
|
|
635
786
|
getPackInstallation,
|
|
636
787
|
getStoredCapabilityHeaderCiphertext,
|
|
637
|
-
|
|
788
|
+
getVariableSet as getVariableSet2,
|
|
638
789
|
listCapabilityCatalogItems,
|
|
639
790
|
listCapabilityInstallations,
|
|
640
791
|
listEnabledMcpCapabilityServers,
|
|
@@ -647,10 +798,7 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
|
647
798
|
|
|
648
799
|
// src/domain/environments.ts
|
|
649
800
|
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
650
|
-
import {
|
|
651
|
-
getWorkspaceEnvironment,
|
|
652
|
-
recordAuditEvent
|
|
653
|
-
} from "@opengeni/db";
|
|
801
|
+
import { getVariableSet, recordAuditEvent } from "@opengeni/db";
|
|
654
802
|
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
655
803
|
var MAX_ENVIRONMENTS_PER_WORKSPACE = 25;
|
|
656
804
|
var MAX_VARIABLES_PER_ENVIRONMENT = 100;
|
|
@@ -671,6 +819,8 @@ var reservedExactNames = /* @__PURE__ */ new Set([
|
|
|
671
819
|
"PERL5LIB",
|
|
672
820
|
"GH_TOKEN",
|
|
673
821
|
"GITHUB_TOKEN",
|
|
822
|
+
"GITLAB_TOKEN",
|
|
823
|
+
"AZURE_DEVOPS_EXT_PAT",
|
|
674
824
|
"GIT_ASKPASS",
|
|
675
825
|
"GIT_TERMINAL_PROMPT"
|
|
676
826
|
]);
|
|
@@ -682,46 +832,52 @@ var reservedPrefixes = [
|
|
|
682
832
|
"LD_",
|
|
683
833
|
"DYLD_"
|
|
684
834
|
];
|
|
685
|
-
function
|
|
835
|
+
function assertAllowedVariableSetVariableName(name) {
|
|
686
836
|
if (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
|
|
687
|
-
throw new HTTPException4(422, {
|
|
837
|
+
throw new HTTPException4(422, {
|
|
838
|
+
message: `reserved variable set variable name / reserved environment variable name: ${name}`
|
|
839
|
+
});
|
|
688
840
|
}
|
|
689
841
|
}
|
|
690
|
-
|
|
842
|
+
var assertAllowedEnvironmentVariableName = assertAllowedVariableSetVariableName;
|
|
843
|
+
function requireVariableSetEncryption(settings) {
|
|
691
844
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
692
845
|
if (!key) {
|
|
693
|
-
throw new HTTPException4(503, {
|
|
846
|
+
throw new HTTPException4(503, {
|
|
847
|
+
message: "variable sets require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
|
|
848
|
+
});
|
|
694
849
|
}
|
|
695
850
|
return key;
|
|
696
851
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
852
|
+
var requireEnvironmentEncryption = requireVariableSetEncryption;
|
|
853
|
+
async function requireVariableSetForApi(db, workspaceId, variableSetId) {
|
|
854
|
+
const variableSet = await getVariableSet(db, workspaceId, variableSetId);
|
|
855
|
+
if (!variableSet) {
|
|
856
|
+
throw new HTTPException4(404, { message: "variableSet not found" });
|
|
701
857
|
}
|
|
702
|
-
return
|
|
858
|
+
return variableSet;
|
|
703
859
|
}
|
|
704
|
-
async function
|
|
705
|
-
|
|
860
|
+
async function validateVariableSetAttachment(deps, grant, workspaceId, variableSetId, options = {}) {
|
|
861
|
+
requireVariableSetEncryption(deps.settings);
|
|
706
862
|
if (!options.preauthorized) {
|
|
707
|
-
requirePermission(grant, "
|
|
863
|
+
requirePermission(grant, "variable-sets:use");
|
|
708
864
|
}
|
|
709
|
-
const
|
|
710
|
-
if (!
|
|
711
|
-
throw new HTTPException4(422, { message: "unknown
|
|
865
|
+
const variableSet = await getVariableSet(deps.db, workspaceId, variableSetId);
|
|
866
|
+
if (!variableSet) {
|
|
867
|
+
throw new HTTPException4(422, { message: "unknown variableSetId" });
|
|
712
868
|
}
|
|
713
|
-
return
|
|
869
|
+
return variableSet;
|
|
714
870
|
}
|
|
715
|
-
async function
|
|
871
|
+
async function recordVariableSetAuditEvent(db, input) {
|
|
716
872
|
await recordAuditEvent(db, {
|
|
717
873
|
accountId: input.grant.accountId,
|
|
718
874
|
workspaceId: input.grant.workspaceId,
|
|
719
875
|
subjectId: input.grant.subjectId,
|
|
720
876
|
action: input.action,
|
|
721
|
-
targetType: "
|
|
722
|
-
targetId: input.
|
|
877
|
+
targetType: "workspace_variable_set",
|
|
878
|
+
targetId: input.variableSetId,
|
|
723
879
|
metadata: {
|
|
724
|
-
|
|
880
|
+
variableSetId: input.variableSetId,
|
|
725
881
|
...input.variableName ? { name: input.variableName } : {}
|
|
726
882
|
}
|
|
727
883
|
});
|
|
@@ -731,7 +887,11 @@ async function recordEnvironmentAuditEvent(db, input) {
|
|
|
731
887
|
import {
|
|
732
888
|
CapabilityPack
|
|
733
889
|
} from "@opengeni/contracts";
|
|
734
|
-
import {
|
|
890
|
+
import {
|
|
891
|
+
getWorkspacePack,
|
|
892
|
+
listPackInstallations,
|
|
893
|
+
listWorkspacePacks
|
|
894
|
+
} from "@opengeni/db";
|
|
735
895
|
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
736
896
|
var MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
|
|
737
897
|
var marketingSocialPack = {
|
|
@@ -780,7 +940,12 @@ var marketingSocialPack = {
|
|
|
780
940
|
category: "social-media",
|
|
781
941
|
authModel: "oauth2_authorization_code",
|
|
782
942
|
providers: ["instagram", "facebook"],
|
|
783
|
-
scopes: [
|
|
943
|
+
scopes: [
|
|
944
|
+
"instagram_basic",
|
|
945
|
+
"instagram_manage_insights",
|
|
946
|
+
"pages_read_engagement",
|
|
947
|
+
"pages_show_list"
|
|
948
|
+
],
|
|
784
949
|
required: false,
|
|
785
950
|
metadata: {
|
|
786
951
|
docs: "https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/"
|
|
@@ -804,7 +969,10 @@ var marketingSocialPack = {
|
|
|
804
969
|
category: "social-media",
|
|
805
970
|
authModel: "oauth2_authorization_code",
|
|
806
971
|
providers: ["youtube"],
|
|
807
|
-
scopes: [
|
|
972
|
+
scopes: [
|
|
973
|
+
"https://www.googleapis.com/auth/youtube.readonly",
|
|
974
|
+
"https://www.googleapis.com/auth/yt-analytics.readonly"
|
|
975
|
+
],
|
|
808
976
|
required: false,
|
|
809
977
|
metadata: {
|
|
810
978
|
docs: "https://developers.google.com/youtube/v3"
|
|
@@ -957,16 +1125,24 @@ async function buildCapabilityCatalog(input) {
|
|
|
957
1125
|
listWorkspaceCapabilityPacks(input.db, input.workspaceId),
|
|
958
1126
|
discoverBundledSkills()
|
|
959
1127
|
]);
|
|
960
|
-
const capabilityInstallationById = new Map(
|
|
961
|
-
|
|
1128
|
+
const capabilityInstallationById = new Map(
|
|
1129
|
+
capabilityInstallations.map((installation) => [installation.capabilityId, installation])
|
|
1130
|
+
);
|
|
1131
|
+
const activePackIds = new Set(
|
|
1132
|
+
packInstallations.filter((installation) => installation.status === "active").map((installation) => installation.packId)
|
|
1133
|
+
);
|
|
962
1134
|
const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
|
|
963
1135
|
const builtIns = [
|
|
964
|
-
...workspacePacks.map(
|
|
1136
|
+
...workspacePacks.map(
|
|
1137
|
+
(pack) => packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual")
|
|
1138
|
+
),
|
|
965
1139
|
...configuredMcpCatalogItems(input.settings),
|
|
966
1140
|
...platformApiCatalogItems(),
|
|
967
1141
|
...bundledSkills
|
|
968
1142
|
];
|
|
969
|
-
const items = dedupeCatalogItems([...builtIns, ...persistedItems]).map(
|
|
1143
|
+
const items = dedupeCatalogItems([...builtIns, ...persistedItems]).map(
|
|
1144
|
+
(item) => applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds)
|
|
1145
|
+
).sort(compareCatalogItems);
|
|
970
1146
|
return {
|
|
971
1147
|
items,
|
|
972
1148
|
installations: capabilityInstallations
|
|
@@ -975,7 +1151,9 @@ async function buildCapabilityCatalog(input) {
|
|
|
975
1151
|
async function createCatalogItem(input) {
|
|
976
1152
|
const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
|
|
977
1153
|
if (id.startsWith("pack:")) {
|
|
978
|
-
throw new HTTPException6(422, {
|
|
1154
|
+
throw new HTTPException6(422, {
|
|
1155
|
+
message: "packs are managed by OpenGeni and cannot be manually created"
|
|
1156
|
+
});
|
|
979
1157
|
}
|
|
980
1158
|
const source = input.payload.source === "built_in" || input.payload.source === "configured" || input.payload.source === "registry" ? "manual" : input.payload.source;
|
|
981
1159
|
const metadata = {
|
|
@@ -1000,9 +1178,16 @@ async function createCatalogItem(input) {
|
|
|
1000
1178
|
});
|
|
1001
1179
|
}
|
|
1002
1180
|
async function enableCapability(input) {
|
|
1003
|
-
const item = await requireCatalogItem(
|
|
1181
|
+
const item = await requireCatalogItem(
|
|
1182
|
+
input.db,
|
|
1183
|
+
input.workspaceId,
|
|
1184
|
+
input.settings,
|
|
1185
|
+
input.capabilityId
|
|
1186
|
+
);
|
|
1004
1187
|
if (item.kind === "mcp" && !item.runtime.available) {
|
|
1005
|
-
throw new HTTPException6(422, {
|
|
1188
|
+
throw new HTTPException6(422, {
|
|
1189
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled"
|
|
1190
|
+
});
|
|
1006
1191
|
}
|
|
1007
1192
|
let installationMetadata = input.payload.metadata;
|
|
1008
1193
|
const installationConfig = { ...input.payload.config };
|
|
@@ -1024,7 +1209,7 @@ async function enableCapability(input) {
|
|
|
1024
1209
|
if (headers) {
|
|
1025
1210
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
1026
1211
|
installationConfig.headersEncrypted = Object.fromEntries(
|
|
1027
|
-
Object.entries(headers).map(([name, value]) => [name,
|
|
1212
|
+
Object.entries(headers).map(([name, value]) => [name, encryptVariableSetValue(key, value)])
|
|
1028
1213
|
);
|
|
1029
1214
|
}
|
|
1030
1215
|
}
|
|
@@ -1036,36 +1221,44 @@ async function enableCapability(input) {
|
|
|
1036
1221
|
}
|
|
1037
1222
|
await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
|
|
1038
1223
|
const existing = await getPackInstallation(input.db, input.workspaceId, packId);
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
if (pack.
|
|
1224
|
+
const storedVariableSetId = typeof existing?.metadata.variableSetId === "string" ? existing.metadata.variableSetId : typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
|
|
1225
|
+
const requestedVariableSetId = input.payload.variableSetId;
|
|
1226
|
+
const variableSetId = requestedVariableSetId ?? storedVariableSetId;
|
|
1227
|
+
if (pack.variableSet?.required && !variableSetId) {
|
|
1043
1228
|
throw new HTTPException6(422, {
|
|
1044
|
-
message: `pack ${packId} requires an
|
|
1229
|
+
message: `pack ${packId} requires an variableSet attachment; pass variableSetId`
|
|
1045
1230
|
});
|
|
1046
1231
|
}
|
|
1047
|
-
if (
|
|
1048
|
-
if (
|
|
1049
|
-
const
|
|
1232
|
+
if (variableSetId) {
|
|
1233
|
+
if (requestedVariableSetId) {
|
|
1234
|
+
const variableSet = await validateVariableSetAttachment(
|
|
1050
1235
|
{ settings: input.settings, db: input.db },
|
|
1051
1236
|
input.grant,
|
|
1052
1237
|
input.workspaceId,
|
|
1053
|
-
|
|
1238
|
+
requestedVariableSetId
|
|
1239
|
+
);
|
|
1240
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
1241
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name)
|
|
1054
1242
|
);
|
|
1055
|
-
const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
1056
1243
|
if (missing.length > 0) {
|
|
1057
|
-
throw new HTTPException6(422, {
|
|
1244
|
+
throw new HTTPException6(422, {
|
|
1245
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`
|
|
1246
|
+
});
|
|
1058
1247
|
}
|
|
1059
1248
|
} else {
|
|
1060
|
-
const
|
|
1061
|
-
if (!
|
|
1249
|
+
const variableSet = await getVariableSet2(input.db, input.workspaceId, variableSetId);
|
|
1250
|
+
if (!variableSet) {
|
|
1062
1251
|
throw new HTTPException6(422, {
|
|
1063
|
-
message: `the stored
|
|
1252
|
+
message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`
|
|
1064
1253
|
});
|
|
1065
1254
|
}
|
|
1066
|
-
const missing = (pack.
|
|
1255
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
1256
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name)
|
|
1257
|
+
);
|
|
1067
1258
|
if (missing.length > 0) {
|
|
1068
|
-
throw new HTTPException6(422, {
|
|
1259
|
+
throw new HTTPException6(422, {
|
|
1260
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`
|
|
1261
|
+
});
|
|
1069
1262
|
}
|
|
1070
1263
|
}
|
|
1071
1264
|
}
|
|
@@ -1076,7 +1269,7 @@ async function enableCapability(input) {
|
|
|
1076
1269
|
metadata: {
|
|
1077
1270
|
...input.payload.metadata,
|
|
1078
1271
|
packVersion: pack.version,
|
|
1079
|
-
...
|
|
1272
|
+
...variableSetId ? { variableSetId } : {}
|
|
1080
1273
|
}
|
|
1081
1274
|
});
|
|
1082
1275
|
}
|
|
@@ -1095,13 +1288,22 @@ async function resolveMcpCredentialHeaders(input, item) {
|
|
|
1095
1288
|
requireCapabilityHeaderEncryption(input.settings);
|
|
1096
1289
|
return provided;
|
|
1097
1290
|
}
|
|
1098
|
-
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
1291
|
+
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
1292
|
+
input.db,
|
|
1293
|
+
input.workspaceId,
|
|
1294
|
+
item.id
|
|
1295
|
+
);
|
|
1099
1296
|
if (!storedCiphertext) {
|
|
1100
1297
|
return null;
|
|
1101
1298
|
}
|
|
1102
1299
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
1103
1300
|
try {
|
|
1104
|
-
return Object.fromEntries(
|
|
1301
|
+
return Object.fromEntries(
|
|
1302
|
+
Object.entries(storedCiphertext).map(([name, value]) => [
|
|
1303
|
+
name,
|
|
1304
|
+
decryptVariableSetValue(key, value)
|
|
1305
|
+
])
|
|
1306
|
+
);
|
|
1105
1307
|
} catch {
|
|
1106
1308
|
throw new HTTPException6(422, {
|
|
1107
1309
|
message: `stored credential headers for "${item.name}" could not be decrypted; supply them again in the enable request "headers" field`
|
|
@@ -1114,7 +1316,9 @@ function normalizedMcpCredentialHeaders(headers) {
|
|
|
1114
1316
|
return null;
|
|
1115
1317
|
}
|
|
1116
1318
|
if (entries.length > maxMcpCredentialHeaders) {
|
|
1117
|
-
throw new HTTPException6(422, {
|
|
1319
|
+
throw new HTTPException6(422, {
|
|
1320
|
+
message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers`
|
|
1321
|
+
});
|
|
1118
1322
|
}
|
|
1119
1323
|
const seen = /* @__PURE__ */ new Set();
|
|
1120
1324
|
for (const [name, value] of entries) {
|
|
@@ -1127,17 +1331,23 @@ function normalizedMcpCredentialHeaders(headers) {
|
|
|
1127
1331
|
}
|
|
1128
1332
|
seen.add(lower);
|
|
1129
1333
|
if (value.length === 0 || value.length > maxMcpCredentialHeaderValueLength) {
|
|
1130
|
-
throw new HTTPException6(422, {
|
|
1334
|
+
throw new HTTPException6(422, {
|
|
1335
|
+
message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters`
|
|
1336
|
+
});
|
|
1131
1337
|
}
|
|
1132
1338
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
1133
|
-
throw new HTTPException6(422, {
|
|
1339
|
+
throw new HTTPException6(422, {
|
|
1340
|
+
message: `credential header ${name} contains forbidden control characters`
|
|
1341
|
+
});
|
|
1134
1342
|
}
|
|
1135
1343
|
}
|
|
1136
1344
|
return Object.fromEntries(entries);
|
|
1137
1345
|
}
|
|
1138
1346
|
async function validateMcpCapabilityConnectionRef(input, item, ref) {
|
|
1139
1347
|
if (ref.subjectScope === "subject") {
|
|
1140
|
-
throw new HTTPException6(422, {
|
|
1348
|
+
throw new HTTPException6(422, {
|
|
1349
|
+
message: "subject-owned connection refs are not supported for agent runtime use yet"
|
|
1350
|
+
});
|
|
1141
1351
|
}
|
|
1142
1352
|
const normalized = {
|
|
1143
1353
|
providerDomain: ref.providerDomain.trim(),
|
|
@@ -1151,26 +1361,43 @@ async function validateMcpCapabilityConnectionRef(input, item, ref) {
|
|
|
1151
1361
|
throw new HTTPException6(422, { message: "connectionRef.providerDomain is required" });
|
|
1152
1362
|
}
|
|
1153
1363
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
1154
|
-
throw new HTTPException6(422, {
|
|
1364
|
+
throw new HTTPException6(422, {
|
|
1365
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef"
|
|
1366
|
+
});
|
|
1155
1367
|
}
|
|
1156
1368
|
if (!normalized.connectionId) {
|
|
1157
1369
|
return normalized;
|
|
1158
1370
|
}
|
|
1159
|
-
const connection = await getConnectionMetadata(
|
|
1371
|
+
const connection = await getConnectionMetadata(
|
|
1372
|
+
input.db,
|
|
1373
|
+
input.workspaceId,
|
|
1374
|
+
normalized.connectionId,
|
|
1375
|
+
input.grant.subjectId
|
|
1376
|
+
);
|
|
1160
1377
|
if (!connection) {
|
|
1161
|
-
throw new HTTPException6(422, {
|
|
1378
|
+
throw new HTTPException6(422, {
|
|
1379
|
+
message: "connectionRef.connectionId does not reference a visible connection"
|
|
1380
|
+
});
|
|
1162
1381
|
}
|
|
1163
1382
|
if (connection.subjectId !== null) {
|
|
1164
|
-
throw new HTTPException6(422, {
|
|
1383
|
+
throw new HTTPException6(422, {
|
|
1384
|
+
message: "agent runtime connection refs must reference workspace-shared connections in I1"
|
|
1385
|
+
});
|
|
1165
1386
|
}
|
|
1166
1387
|
if (connection.status !== "active") {
|
|
1167
|
-
throw new HTTPException6(422, {
|
|
1388
|
+
throw new HTTPException6(422, {
|
|
1389
|
+
message: `connectionRef.connectionId is not active (${connection.status})`
|
|
1390
|
+
});
|
|
1168
1391
|
}
|
|
1169
1392
|
if (connection.providerDomain !== normalized.providerDomain) {
|
|
1170
|
-
throw new HTTPException6(422, {
|
|
1393
|
+
throw new HTTPException6(422, {
|
|
1394
|
+
message: "connectionRef.providerDomain does not match the referenced connection"
|
|
1395
|
+
});
|
|
1171
1396
|
}
|
|
1172
1397
|
if (normalized.kind && connection.kind !== normalized.kind) {
|
|
1173
|
-
throw new HTTPException6(422, {
|
|
1398
|
+
throw new HTTPException6(422, {
|
|
1399
|
+
message: "connectionRef.kind does not match the referenced connection"
|
|
1400
|
+
});
|
|
1174
1401
|
}
|
|
1175
1402
|
return normalized;
|
|
1176
1403
|
}
|
|
@@ -1210,7 +1437,9 @@ function requiredCapabilityHeaders(metadata) {
|
|
|
1210
1437
|
function requireCapabilityHeaderEncryption(settings) {
|
|
1211
1438
|
const key = environmentsEncryptionKeyBytes2(settings);
|
|
1212
1439
|
if (!key) {
|
|
1213
|
-
throw new HTTPException6(503, {
|
|
1440
|
+
throw new HTTPException6(503, {
|
|
1441
|
+
message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
|
|
1442
|
+
});
|
|
1214
1443
|
}
|
|
1215
1444
|
return key;
|
|
1216
1445
|
}
|
|
@@ -1219,7 +1448,9 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
|
|
|
1219
1448
|
return {};
|
|
1220
1449
|
}
|
|
1221
1450
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
1222
|
-
throw new HTTPException6(422, {
|
|
1451
|
+
throw new HTTPException6(422, {
|
|
1452
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled"
|
|
1453
|
+
});
|
|
1223
1454
|
}
|
|
1224
1455
|
try {
|
|
1225
1456
|
const result = await probe({
|
|
@@ -1245,7 +1476,10 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
|
|
|
1245
1476
|
async function probeStreamableHttpMcpServer(input) {
|
|
1246
1477
|
const controller = new AbortController();
|
|
1247
1478
|
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
1248
|
-
const client = new Client(
|
|
1479
|
+
const client = new Client(
|
|
1480
|
+
{ name: "opengeni-capability-probe", version: "0.1.0" },
|
|
1481
|
+
{ capabilities: {} }
|
|
1482
|
+
);
|
|
1249
1483
|
try {
|
|
1250
1484
|
const transport = new StreamableHTTPClientTransport(new URL(input.url), {
|
|
1251
1485
|
requestInit: {
|
|
@@ -1253,8 +1487,14 @@ async function probeStreamableHttpMcpServer(input) {
|
|
|
1253
1487
|
...input.headers ? { headers: input.headers } : {}
|
|
1254
1488
|
}
|
|
1255
1489
|
});
|
|
1256
|
-
await client.connect(transport, {
|
|
1257
|
-
|
|
1490
|
+
await client.connect(transport, {
|
|
1491
|
+
timeout: input.timeoutMs,
|
|
1492
|
+
maxTotalTimeout: input.timeoutMs
|
|
1493
|
+
});
|
|
1494
|
+
const tools = await client.listTools(void 0, {
|
|
1495
|
+
timeout: input.timeoutMs,
|
|
1496
|
+
maxTotalTimeout: input.timeoutMs
|
|
1497
|
+
});
|
|
1258
1498
|
return { toolCount: tools.tools.length };
|
|
1259
1499
|
} finally {
|
|
1260
1500
|
clearTimeout(timeout);
|
|
@@ -1264,18 +1504,32 @@ async function probeStreamableHttpMcpServer(input) {
|
|
|
1264
1504
|
function mcpProbeErrorMessage(error, endpointUrl) {
|
|
1265
1505
|
const message = error instanceof Error ? error.message : String(error);
|
|
1266
1506
|
const normalized = message.replace(/\s+/g, " ").trim();
|
|
1267
|
-
if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
|
|
1507
|
+
if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
|
|
1508
|
+
normalized
|
|
1509
|
+
)) {
|
|
1268
1510
|
return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
|
|
1269
1511
|
}
|
|
1270
1512
|
return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
|
|
1271
1513
|
}
|
|
1272
1514
|
async function disableCapability(input) {
|
|
1273
|
-
const item = await requireCatalogItem(
|
|
1515
|
+
const item = await requireCatalogItem(
|
|
1516
|
+
input.db,
|
|
1517
|
+
input.workspaceId,
|
|
1518
|
+
input.settings,
|
|
1519
|
+
input.capabilityId
|
|
1520
|
+
);
|
|
1274
1521
|
if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
|
|
1275
|
-
throw new HTTPException6(409, {
|
|
1522
|
+
throw new HTTPException6(409, {
|
|
1523
|
+
message: "built-in and configured capabilities are always available; remove them from configuration to disable them"
|
|
1524
|
+
});
|
|
1276
1525
|
}
|
|
1277
1526
|
if (item.kind === "pack") {
|
|
1278
|
-
await updatePackInstallationStatus(
|
|
1527
|
+
await updatePackInstallationStatus(
|
|
1528
|
+
input.db,
|
|
1529
|
+
input.workspaceId,
|
|
1530
|
+
packIdFromCapabilityId(item.id),
|
|
1531
|
+
"disabled"
|
|
1532
|
+
).catch(() => void 0);
|
|
1279
1533
|
if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
|
|
1280
1534
|
await enableCapabilityInstallation(input.db, {
|
|
1281
1535
|
accountId: input.accountId,
|
|
@@ -1306,16 +1560,18 @@ function settingsWithMcpCapabilityServers(settings, enabled) {
|
|
|
1306
1560
|
if (headers === "unavailable" && !server.connectionRef) {
|
|
1307
1561
|
return [];
|
|
1308
1562
|
}
|
|
1309
|
-
return [
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1563
|
+
return [
|
|
1564
|
+
{
|
|
1565
|
+
id: server.id,
|
|
1566
|
+
name: server.name,
|
|
1567
|
+
url: server.url,
|
|
1568
|
+
...server.allowedTools ? { allowedTools: server.allowedTools } : {},
|
|
1569
|
+
...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
|
|
1570
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
1571
|
+
...headers && headers !== "unavailable" ? { headers } : {},
|
|
1572
|
+
...server.connectionRef ? { connectionRef: server.connectionRef } : {}
|
|
1573
|
+
}
|
|
1574
|
+
];
|
|
1319
1575
|
});
|
|
1320
1576
|
return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
|
|
1321
1577
|
}
|
|
@@ -1369,7 +1625,10 @@ async function discoverMcpRegistryCapabilities(input) {
|
|
|
1369
1625
|
async function fetchMcpRegistryPage(url, options = {}) {
|
|
1370
1626
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1371
1627
|
const controller = new AbortController();
|
|
1372
|
-
const timeout = setTimeout(
|
|
1628
|
+
const timeout = setTimeout(
|
|
1629
|
+
() => controller.abort(),
|
|
1630
|
+
options.timeoutMs ?? mcpRegistryFetchTimeoutMs
|
|
1631
|
+
);
|
|
1373
1632
|
try {
|
|
1374
1633
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
1375
1634
|
if (!response.ok) {
|
|
@@ -1426,28 +1685,30 @@ function packCatalogItem(pack, source) {
|
|
|
1426
1685
|
});
|
|
1427
1686
|
}
|
|
1428
1687
|
function configuredMcpCatalogItems(settings) {
|
|
1429
|
-
return settings.mcpServers.map(
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1688
|
+
return settings.mcpServers.map(
|
|
1689
|
+
(server) => CapabilityCatalogItem.parse({
|
|
1690
|
+
id: `mcp:${server.id}`,
|
|
1691
|
+
kind: "mcp",
|
|
1692
|
+
source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
|
|
1693
|
+
name: server.name ?? server.id,
|
|
1694
|
+
description: firstPartyMcpDescription(server.id),
|
|
1695
|
+
category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
|
|
1696
|
+
tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
|
|
1697
|
+
endpointUrl: server.url,
|
|
1698
|
+
tools: [{ kind: "mcp", id: server.id }],
|
|
1699
|
+
runtime: {
|
|
1700
|
+
available: true,
|
|
1701
|
+
mcpServerId: server.id,
|
|
1702
|
+
transport: "streamable-http",
|
|
1703
|
+
notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
|
|
1704
|
+
},
|
|
1705
|
+
metadata: {
|
|
1706
|
+
mcpServerId: server.id,
|
|
1707
|
+
allowedTools: server.allowedTools ?? [],
|
|
1708
|
+
cacheToolsList: server.cacheToolsList
|
|
1709
|
+
}
|
|
1710
|
+
})
|
|
1711
|
+
);
|
|
1451
1712
|
}
|
|
1452
1713
|
function platformApiCatalogItems() {
|
|
1453
1714
|
return [
|
|
@@ -1483,46 +1744,56 @@ function platformApiCatalogItems() {
|
|
|
1483
1744
|
tags: ["api", "schedules", "agents"],
|
|
1484
1745
|
endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks"
|
|
1485
1746
|
}
|
|
1486
|
-
].map(
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1747
|
+
].map(
|
|
1748
|
+
(item) => CapabilityCatalogItem.parse({
|
|
1749
|
+
id: item.id,
|
|
1750
|
+
name: item.name,
|
|
1751
|
+
description: item.description,
|
|
1752
|
+
category: item.category,
|
|
1753
|
+
tags: item.tags,
|
|
1754
|
+
kind: "api",
|
|
1755
|
+
source: "built_in",
|
|
1756
|
+
runtime: {
|
|
1757
|
+
available: true,
|
|
1758
|
+
notes: "Available through the OpenGeni API."
|
|
1759
|
+
},
|
|
1760
|
+
metadata: {
|
|
1761
|
+
endpointPath: item.endpointPath
|
|
1762
|
+
}
|
|
1763
|
+
})
|
|
1764
|
+
);
|
|
1502
1765
|
}
|
|
1503
1766
|
async function discoverBundledSkills() {
|
|
1504
|
-
const skillsDir = new URL(
|
|
1767
|
+
const skillsDir = new URL(
|
|
1768
|
+
"../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
|
|
1769
|
+
import.meta.url
|
|
1770
|
+
);
|
|
1505
1771
|
try {
|
|
1506
1772
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1507
|
-
const skills = await Promise.all(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1773
|
+
const skills = await Promise.all(
|
|
1774
|
+
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
1775
|
+
const skill = await readSkillMetadata(
|
|
1776
|
+
new URL(`${entry.name}/SKILL.md`, skillsDir),
|
|
1777
|
+
entry.name
|
|
1778
|
+
);
|
|
1779
|
+
return CapabilityCatalogItem.parse({
|
|
1780
|
+
id: `skill:${entry.name}`,
|
|
1781
|
+
kind: "skill",
|
|
1782
|
+
source: "built_in",
|
|
1783
|
+
name: skill.name,
|
|
1784
|
+
description: skill.description,
|
|
1785
|
+
category: skill.category,
|
|
1786
|
+
tags: ["skill", skill.category],
|
|
1787
|
+
runtime: {
|
|
1788
|
+
available: true,
|
|
1789
|
+
notes: "Bundled into the sandbox skill library."
|
|
1790
|
+
},
|
|
1791
|
+
metadata: {
|
|
1792
|
+
path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
})
|
|
1796
|
+
);
|
|
1526
1797
|
return skills;
|
|
1527
1798
|
} catch {
|
|
1528
1799
|
return [];
|
|
@@ -1599,7 +1870,11 @@ function firstPartyMcpDescription(id) {
|
|
|
1599
1870
|
return null;
|
|
1600
1871
|
}
|
|
1601
1872
|
function generatedCapabilityId(payload) {
|
|
1602
|
-
const source = [
|
|
1873
|
+
const source = [
|
|
1874
|
+
payload.kind,
|
|
1875
|
+
payload.name,
|
|
1876
|
+
payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""
|
|
1877
|
+
].join(":");
|
|
1603
1878
|
return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
|
|
1604
1879
|
}
|
|
1605
1880
|
function publicRegistryCapabilityId(name, version, endpointUrl) {
|
|
@@ -1637,7 +1912,9 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1637
1912
|
if (official?.isLatest === false) {
|
|
1638
1913
|
return null;
|
|
1639
1914
|
}
|
|
1640
|
-
const remote = server.remotes?.find(
|
|
1915
|
+
const remote = server.remotes?.find(
|
|
1916
|
+
(candidate) => candidate.type === "streamable-http" && candidate.url
|
|
1917
|
+
);
|
|
1641
1918
|
const endpointUrl = validUrl(remote?.url);
|
|
1642
1919
|
if (!remote || !endpointUrl) {
|
|
1643
1920
|
return null;
|
|
@@ -1654,7 +1931,12 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1654
1931
|
name: server.title || server.name,
|
|
1655
1932
|
description: server.description ?? null,
|
|
1656
1933
|
category: "public-mcp",
|
|
1657
|
-
tags: [
|
|
1934
|
+
tags: [
|
|
1935
|
+
"mcp",
|
|
1936
|
+
"public",
|
|
1937
|
+
"registry",
|
|
1938
|
+
...requiredHeaders.length ? ["requires-credentials"] : []
|
|
1939
|
+
],
|
|
1658
1940
|
homepageUrl,
|
|
1659
1941
|
endpointUrl,
|
|
1660
1942
|
installUrl: homepageUrl,
|
|
@@ -1732,6 +2014,367 @@ function storedConnectionRef(config) {
|
|
|
1732
2014
|
return !!ref && typeof ref === "object" && !Array.isArray(ref) && typeof ref.providerDomain === "string";
|
|
1733
2015
|
}
|
|
1734
2016
|
|
|
2017
|
+
// src/rigs/index.ts
|
|
2018
|
+
import {
|
|
2019
|
+
activateRigVersion,
|
|
2020
|
+
countRigs,
|
|
2021
|
+
createRig,
|
|
2022
|
+
createRigChange,
|
|
2023
|
+
createRigVersion,
|
|
2024
|
+
createRigVersionForChangePromotion,
|
|
2025
|
+
deleteRigIfNoActiveSessions,
|
|
2026
|
+
getRig,
|
|
2027
|
+
getRigByName,
|
|
2028
|
+
getRigChange,
|
|
2029
|
+
getRigVersion,
|
|
2030
|
+
getVariableSet as getVariableSet3,
|
|
2031
|
+
listRigChanges,
|
|
2032
|
+
listRigVersions,
|
|
2033
|
+
recordAuditEvent as recordAuditEvent2,
|
|
2034
|
+
RigActiveVersionChangedError,
|
|
2035
|
+
RigChangeTransitionError,
|
|
2036
|
+
updateRig
|
|
2037
|
+
} from "@opengeni/db";
|
|
2038
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
2039
|
+
var MAX_RIGS_PER_WORKSPACE = 50;
|
|
2040
|
+
var MAX_CHECKS_PER_RIG = 100;
|
|
2041
|
+
var MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
|
|
2042
|
+
var MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
|
|
2043
|
+
async function recordRigAuditEvent(db, input) {
|
|
2044
|
+
await recordAuditEvent2(db, {
|
|
2045
|
+
accountId: input.grant.accountId,
|
|
2046
|
+
workspaceId: input.grant.workspaceId,
|
|
2047
|
+
subjectId: input.grant.subjectId,
|
|
2048
|
+
action: input.action,
|
|
2049
|
+
targetType: "rig",
|
|
2050
|
+
targetId: input.rigId,
|
|
2051
|
+
metadata: { rigId: input.rigId, ...input.metadata ?? {} }
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
function rigActorForGrant(grant) {
|
|
2055
|
+
return `user:${grant.subjectId}`;
|
|
2056
|
+
}
|
|
2057
|
+
async function requireRigForApi(db, workspaceId, rigId) {
|
|
2058
|
+
const rig = await getRig(db, workspaceId, rigId);
|
|
2059
|
+
if (!rig) {
|
|
2060
|
+
throw new HTTPException7(404, { message: "rig not found" });
|
|
2061
|
+
}
|
|
2062
|
+
return rig;
|
|
2063
|
+
}
|
|
2064
|
+
async function requireRigChangeForApi(db, workspaceId, rigId, changeId) {
|
|
2065
|
+
const change = await getRigChange(db, workspaceId, changeId);
|
|
2066
|
+
if (!change || change.rigId !== rigId) {
|
|
2067
|
+
throw new HTTPException7(404, { message: "rig change not found" });
|
|
2068
|
+
}
|
|
2069
|
+
return change;
|
|
2070
|
+
}
|
|
2071
|
+
function trimmedRigName(name) {
|
|
2072
|
+
const trimmed = name.trim();
|
|
2073
|
+
if (!trimmed) {
|
|
2074
|
+
throw new HTTPException7(422, { message: "rig name is required" });
|
|
2075
|
+
}
|
|
2076
|
+
return trimmed;
|
|
2077
|
+
}
|
|
2078
|
+
function assertUniqueCheckNames(checks) {
|
|
2079
|
+
if (!checks) {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2083
|
+
for (const check of checks) {
|
|
2084
|
+
if (seen.has(check.name)) {
|
|
2085
|
+
throw new HTTPException7(422, { message: `duplicate rig check name: ${check.name}` });
|
|
2086
|
+
}
|
|
2087
|
+
seen.add(check.name);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
async function assertVariableSetsExist(db, workspaceId, ids) {
|
|
2091
|
+
if (!ids || ids.length === 0) {
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const unique = [...new Set(ids)];
|
|
2095
|
+
for (const id of unique) {
|
|
2096
|
+
const variableSet = await getVariableSet3(db, workspaceId, id);
|
|
2097
|
+
if (!variableSet) {
|
|
2098
|
+
throw new HTTPException7(422, { message: `unknown defaultVariableSetId: ${id}` });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async function createRigForApi(deps, grant, payload) {
|
|
2103
|
+
const workspaceId = grant.workspaceId;
|
|
2104
|
+
const name = trimmedRigName(payload.name);
|
|
2105
|
+
assertUniqueCheckNames(payload.checks);
|
|
2106
|
+
await assertVariableSetsExist(deps.db, workspaceId, payload.defaultVariableSetIds);
|
|
2107
|
+
if (await countRigs(deps.db, workspaceId) >= MAX_RIGS_PER_WORKSPACE) {
|
|
2108
|
+
throw new HTTPException7(422, {
|
|
2109
|
+
message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
if (await getRigByName(deps.db, workspaceId, name)) {
|
|
2113
|
+
throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
|
|
2114
|
+
}
|
|
2115
|
+
const createdBy = rigActorForGrant(grant);
|
|
2116
|
+
const rig = await createRig(deps.db, {
|
|
2117
|
+
accountId: grant.accountId,
|
|
2118
|
+
workspaceId,
|
|
2119
|
+
name,
|
|
2120
|
+
description: payload.description ?? null,
|
|
2121
|
+
createdBy,
|
|
2122
|
+
initialVersion: {
|
|
2123
|
+
image: payload.image ?? null,
|
|
2124
|
+
setupScript: payload.setupScript ?? null,
|
|
2125
|
+
checks: payload.checks,
|
|
2126
|
+
credentialHooks: payload.credentialHooks,
|
|
2127
|
+
defaultVariableSetIds: payload.defaultVariableSetIds,
|
|
2128
|
+
changelog: "Initial version",
|
|
2129
|
+
createdBy
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.created", rigId: rig.id });
|
|
2133
|
+
return rig;
|
|
2134
|
+
}
|
|
2135
|
+
async function updateRigForApi(deps, grant, rig, payload) {
|
|
2136
|
+
const workspaceId = grant.workspaceId;
|
|
2137
|
+
const name = payload.name !== void 0 ? trimmedRigName(payload.name) : void 0;
|
|
2138
|
+
if (name !== void 0 && name !== rig.name) {
|
|
2139
|
+
const existing = await getRigByName(deps.db, workspaceId, name);
|
|
2140
|
+
if (existing && existing.id !== rig.id) {
|
|
2141
|
+
throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const updated = await updateRig(deps.db, workspaceId, rig.id, {
|
|
2145
|
+
...name !== void 0 ? { name } : {},
|
|
2146
|
+
...payload.description !== void 0 ? { description: payload.description } : {}
|
|
2147
|
+
});
|
|
2148
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.updated", rigId: rig.id });
|
|
2149
|
+
return updated;
|
|
2150
|
+
}
|
|
2151
|
+
async function deleteRigForApi(deps, grant, rig) {
|
|
2152
|
+
const workspaceId = grant.workspaceId;
|
|
2153
|
+
const deleted = await deleteRigIfNoActiveSessions(deps.db, workspaceId, rig.id);
|
|
2154
|
+
if (deleted.activeSessionCount > 0) {
|
|
2155
|
+
throw new HTTPException7(409, {
|
|
2156
|
+
message: `rig is referenced by ${deleted.activeSessionCount} active session(s); it cannot be deleted`
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
if (!deleted.deleted) {
|
|
2160
|
+
throw new HTTPException7(404, { message: "rig not found" });
|
|
2161
|
+
}
|
|
2162
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.deleted", rigId: rig.id });
|
|
2163
|
+
}
|
|
2164
|
+
async function proposeRigChangeForApi(deps, grant, rig, request, options = {}) {
|
|
2165
|
+
const workspaceId = grant.workspaceId;
|
|
2166
|
+
if (!rig.activeVersion) {
|
|
2167
|
+
throw new HTTPException7(422, { message: "rig has no active version to base a change on" });
|
|
2168
|
+
}
|
|
2169
|
+
if (request.kind === "definition_edit") {
|
|
2170
|
+
assertUniqueCheckNames(request.payload.checks);
|
|
2171
|
+
await assertVariableSetsExist(
|
|
2172
|
+
deps.db,
|
|
2173
|
+
workspaceId,
|
|
2174
|
+
request.payload.defaultVariableSetIds ?? void 0
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
const change = await createRigChange(deps.db, {
|
|
2178
|
+
accountId: grant.accountId,
|
|
2179
|
+
workspaceId,
|
|
2180
|
+
rigId: rig.id,
|
|
2181
|
+
baseVersionId: rig.activeVersion.id,
|
|
2182
|
+
kind: request.kind,
|
|
2183
|
+
payload: request.payload,
|
|
2184
|
+
proposedBy: options.proposedBy ?? rigActorForGrant(grant)
|
|
2185
|
+
});
|
|
2186
|
+
await recordRigAuditEvent(deps.db, {
|
|
2187
|
+
grant,
|
|
2188
|
+
action: "rig.change.proposed",
|
|
2189
|
+
rigId: rig.id,
|
|
2190
|
+
metadata: { changeId: change.id, kind: change.kind }
|
|
2191
|
+
});
|
|
2192
|
+
return change;
|
|
2193
|
+
}
|
|
2194
|
+
function classifyRigVerificationOutcome(input) {
|
|
2195
|
+
if (input.infraError) {
|
|
2196
|
+
return { status: "failed", action: "retryable_failure" };
|
|
2197
|
+
}
|
|
2198
|
+
if (!input.passed) {
|
|
2199
|
+
return { status: "rejected", action: "reject" };
|
|
2200
|
+
}
|
|
2201
|
+
if (input.kind === "setup_append") {
|
|
2202
|
+
return { status: "merged", action: "auto_promote" };
|
|
2203
|
+
}
|
|
2204
|
+
return { status: "proposed", action: "await_manage_promote" };
|
|
2205
|
+
}
|
|
2206
|
+
function appendRigSetupCommand(baseSetupScript, command) {
|
|
2207
|
+
const base = (baseSetupScript ?? "").trimEnd();
|
|
2208
|
+
return base ? `${base}
|
|
2209
|
+
${command}` : command;
|
|
2210
|
+
}
|
|
2211
|
+
async function promoteChangeWithActiveCas(deps, workspaceId, rigId, changeId, input) {
|
|
2212
|
+
try {
|
|
2213
|
+
return await createRigVersionForChangePromotion(deps.db, workspaceId, rigId, changeId, input);
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
if (error instanceof RigActiveVersionChangedError) {
|
|
2216
|
+
throw new HTTPException7(409, {
|
|
2217
|
+
message: `rig moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
if (error instanceof RigChangeTransitionError) {
|
|
2221
|
+
throw new HTTPException7(409, { message: error.message });
|
|
2222
|
+
}
|
|
2223
|
+
throw error;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async function promoteSetupAppendChange(deps, grant, rig, change) {
|
|
2227
|
+
if (change.kind !== "setup_append") {
|
|
2228
|
+
throw new HTTPException7(422, {
|
|
2229
|
+
message: "only setup_append changes auto-promote through this path"
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
if (change.status !== "proposed" && change.status !== "verifying") {
|
|
2233
|
+
throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
|
|
2234
|
+
}
|
|
2235
|
+
if (!change.baseVersionId) {
|
|
2236
|
+
throw new HTTPException7(422, { message: "rig change has no base version" });
|
|
2237
|
+
}
|
|
2238
|
+
const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
|
|
2239
|
+
if (!base) {
|
|
2240
|
+
throw new HTTPException7(404, { message: "base rig version not found" });
|
|
2241
|
+
}
|
|
2242
|
+
const payload = change.payload;
|
|
2243
|
+
if (typeof payload.command !== "string" || !payload.command.trim()) {
|
|
2244
|
+
throw new HTTPException7(422, { message: "setup_append change is missing command" });
|
|
2245
|
+
}
|
|
2246
|
+
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
2247
|
+
deps,
|
|
2248
|
+
grant.workspaceId,
|
|
2249
|
+
rig.id,
|
|
2250
|
+
change.id,
|
|
2251
|
+
{
|
|
2252
|
+
expectedActiveVersionId: change.baseVersionId,
|
|
2253
|
+
image: base.image,
|
|
2254
|
+
setupScript: appendRigSetupCommand(base.setupScript, payload.command),
|
|
2255
|
+
checks: base.checks,
|
|
2256
|
+
credentialHooks: base.credentialHooks,
|
|
2257
|
+
defaultVariableSetIds: base.defaultVariableSetIds,
|
|
2258
|
+
changelog: typeof payload.note === "string" && payload.note.trim() ? payload.note : "Verified setup append",
|
|
2259
|
+
createdBy: change.proposedBy ?? rigActorForGrant(grant)
|
|
2260
|
+
}
|
|
2261
|
+
);
|
|
2262
|
+
await recordRigAuditEvent(deps.db, {
|
|
2263
|
+
grant,
|
|
2264
|
+
action: "rig.change.merged",
|
|
2265
|
+
rigId: rig.id,
|
|
2266
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2267
|
+
});
|
|
2268
|
+
await recordRigAuditEvent(deps.db, {
|
|
2269
|
+
grant,
|
|
2270
|
+
action: "rig.version.promoted",
|
|
2271
|
+
rigId: rig.id,
|
|
2272
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2273
|
+
});
|
|
2274
|
+
return { change: updated, version };
|
|
2275
|
+
}
|
|
2276
|
+
async function promoteVerifiedDefinitionEditChangeForApi(deps, grant, rig, change) {
|
|
2277
|
+
if (change.kind !== "definition_edit") {
|
|
2278
|
+
throw new HTTPException7(422, { message: "only definition_edit changes use explicit promote" });
|
|
2279
|
+
}
|
|
2280
|
+
if (change.status !== "proposed") {
|
|
2281
|
+
throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
|
|
2282
|
+
}
|
|
2283
|
+
if (change.verification?.passed !== true) {
|
|
2284
|
+
throw new HTTPException7(422, {
|
|
2285
|
+
message: "definition_edit change must pass verification before promote"
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
if (!change.baseVersionId) {
|
|
2289
|
+
throw new HTTPException7(422, { message: "rig change has no base version" });
|
|
2290
|
+
}
|
|
2291
|
+
const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
|
|
2292
|
+
if (!base) {
|
|
2293
|
+
throw new HTTPException7(404, { message: "base rig version not found" });
|
|
2294
|
+
}
|
|
2295
|
+
const payload = change.payload;
|
|
2296
|
+
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
2297
|
+
deps,
|
|
2298
|
+
grant.workspaceId,
|
|
2299
|
+
rig.id,
|
|
2300
|
+
change.id,
|
|
2301
|
+
{
|
|
2302
|
+
expectedActiveVersionId: change.baseVersionId,
|
|
2303
|
+
image: payload.image === void 0 ? base.image : payload.image,
|
|
2304
|
+
setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
|
|
2305
|
+
checks: Array.isArray(payload.checks) ? payload.checks : base.checks,
|
|
2306
|
+
credentialHooks: Array.isArray(payload.credentialHooks) ? payload.credentialHooks : base.credentialHooks,
|
|
2307
|
+
defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds) ? payload.defaultVariableSetIds : base.defaultVariableSetIds,
|
|
2308
|
+
changelog: typeof payload.changelog === "string" && payload.changelog.trim() ? payload.changelog : "Verified definition edit",
|
|
2309
|
+
createdBy: rigActorForGrant(grant)
|
|
2310
|
+
}
|
|
2311
|
+
);
|
|
2312
|
+
await recordRigAuditEvent(deps.db, {
|
|
2313
|
+
grant,
|
|
2314
|
+
action: "rig.change.merged",
|
|
2315
|
+
rigId: rig.id,
|
|
2316
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2317
|
+
});
|
|
2318
|
+
await recordRigAuditEvent(deps.db, {
|
|
2319
|
+
grant,
|
|
2320
|
+
action: "rig.version.promoted",
|
|
2321
|
+
rigId: rig.id,
|
|
2322
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2323
|
+
});
|
|
2324
|
+
return { change: updated, version };
|
|
2325
|
+
}
|
|
2326
|
+
async function createRigVersionForApi(deps, grant, rig, payload) {
|
|
2327
|
+
if (!rig.activeVersion) {
|
|
2328
|
+
throw new HTTPException7(422, { message: "rig has no active version" });
|
|
2329
|
+
}
|
|
2330
|
+
assertUniqueCheckNames(payload.checks);
|
|
2331
|
+
await assertVariableSetsExist(
|
|
2332
|
+
deps.db,
|
|
2333
|
+
grant.workspaceId,
|
|
2334
|
+
payload.defaultVariableSetIds ?? void 0
|
|
2335
|
+
);
|
|
2336
|
+
const base = rig.activeVersion;
|
|
2337
|
+
const version = await createRigVersion(
|
|
2338
|
+
deps.db,
|
|
2339
|
+
grant.workspaceId,
|
|
2340
|
+
rig.id,
|
|
2341
|
+
{
|
|
2342
|
+
image: payload.image === void 0 ? base.image : payload.image,
|
|
2343
|
+
setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
|
|
2344
|
+
checks: payload.checks ?? base.checks,
|
|
2345
|
+
credentialHooks: payload.credentialHooks ?? base.credentialHooks,
|
|
2346
|
+
defaultVariableSetIds: payload.defaultVariableSetIds ?? base.defaultVariableSetIds,
|
|
2347
|
+
changelog: payload.changelog ?? "Manager-created version",
|
|
2348
|
+
createdBy: rigActorForGrant(grant)
|
|
2349
|
+
},
|
|
2350
|
+
{ activate: true }
|
|
2351
|
+
);
|
|
2352
|
+
await recordRigAuditEvent(deps.db, {
|
|
2353
|
+
grant,
|
|
2354
|
+
action: "rig.version.promoted",
|
|
2355
|
+
rigId: rig.id,
|
|
2356
|
+
metadata: { versionId: version.id, version: version.version, direct: true }
|
|
2357
|
+
});
|
|
2358
|
+
return version;
|
|
2359
|
+
}
|
|
2360
|
+
async function activateRigVersionForApi(deps, grant, rig, versionId) {
|
|
2361
|
+
const workspaceId = grant.workspaceId;
|
|
2362
|
+
const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
|
|
2363
|
+
await recordRigAuditEvent(deps.db, {
|
|
2364
|
+
grant,
|
|
2365
|
+
action: "rig.version.activated",
|
|
2366
|
+
rigId: rig.id,
|
|
2367
|
+
metadata: { versionId: version.id, version: version.version }
|
|
2368
|
+
});
|
|
2369
|
+
return version;
|
|
2370
|
+
}
|
|
2371
|
+
async function listRigVersionsForApi(deps, workspaceId, rigId) {
|
|
2372
|
+
return await listRigVersions(deps.db, workspaceId, rigId);
|
|
2373
|
+
}
|
|
2374
|
+
async function listRigChangesForApi(deps, workspaceId, rigId, limit) {
|
|
2375
|
+
return await listRigChanges(deps.db, workspaceId, rigId, limit);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
1735
2378
|
// src/domain/resources.ts
|
|
1736
2379
|
import {
|
|
1737
2380
|
mergeResourceRefs as mergeContractResourceRefs,
|
|
@@ -1740,26 +2383,27 @@ import {
|
|
|
1740
2383
|
ResourceRefConflictError,
|
|
1741
2384
|
stableJson
|
|
1742
2385
|
} from "@opengeni/contracts";
|
|
1743
|
-
import {
|
|
1744
|
-
|
|
1745
|
-
requireFile
|
|
1746
|
-
} from "@opengeni/db";
|
|
1747
|
-
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
2386
|
+
import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
|
|
2387
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
1748
2388
|
function validateToolRefs(tools, settings) {
|
|
1749
2389
|
const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
1750
2390
|
const out = [];
|
|
1751
2391
|
for (const tool of tools) {
|
|
1752
2392
|
if (tool.kind !== "mcp") {
|
|
1753
|
-
throw new
|
|
2393
|
+
throw new HTTPException8(422, {
|
|
2394
|
+
message: `unsupported tool kind: ${tool.kind}`
|
|
2395
|
+
});
|
|
1754
2396
|
}
|
|
1755
2397
|
const optional = tool.optional === true;
|
|
1756
2398
|
if (!mcpServerIds.has(tool.id)) {
|
|
1757
2399
|
if (optional) {
|
|
1758
2400
|
continue;
|
|
1759
2401
|
}
|
|
1760
|
-
throw new
|
|
2402
|
+
throw new HTTPException8(422, { message: `unknown MCP server id: ${tool.id}` });
|
|
1761
2403
|
}
|
|
1762
|
-
out.push(
|
|
2404
|
+
out.push(
|
|
2405
|
+
optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id }
|
|
2406
|
+
);
|
|
1763
2407
|
}
|
|
1764
2408
|
return mergeToolRefs([], out);
|
|
1765
2409
|
}
|
|
@@ -1787,12 +2431,12 @@ function normalizeResources(resources) {
|
|
|
1787
2431
|
} else {
|
|
1788
2432
|
const url = parseResourceUrl(resource.uri);
|
|
1789
2433
|
if (url.protocol !== "https:" || !url.hostname) {
|
|
1790
|
-
throw new
|
|
2434
|
+
throw new HTTPException8(422, { message: "repository resources must use HTTPS Git URLs" });
|
|
1791
2435
|
}
|
|
1792
2436
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1793
2437
|
const parts = path.split("/").filter(Boolean);
|
|
1794
2438
|
if (parts.length < 2) {
|
|
1795
|
-
throw new
|
|
2439
|
+
throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
|
|
1796
2440
|
}
|
|
1797
2441
|
const repo = parts.join("/");
|
|
1798
2442
|
const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
|
|
@@ -1802,6 +2446,11 @@ function normalizeResources(resources) {
|
|
|
1802
2446
|
ref: resource.ref.trim(),
|
|
1803
2447
|
mountPath,
|
|
1804
2448
|
...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
|
|
2449
|
+
...resource.provider ? { provider: resource.provider } : {},
|
|
2450
|
+
...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
|
|
2451
|
+
...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
|
|
2452
|
+
...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
|
|
2453
|
+
...resource.connectionId ? { connectionId: resource.connectionId } : {},
|
|
1805
2454
|
...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
|
|
1806
2455
|
...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
|
|
1807
2456
|
};
|
|
@@ -1809,7 +2458,9 @@ function normalizeResources(resources) {
|
|
|
1809
2458
|
const key = stableJson(normalized);
|
|
1810
2459
|
const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
|
|
1811
2460
|
if (mounted && mounted !== key) {
|
|
1812
|
-
throw new
|
|
2461
|
+
throw new HTTPException8(422, {
|
|
2462
|
+
message: `duplicate resource mount path: ${normalized.mountPath}`
|
|
2463
|
+
});
|
|
1813
2464
|
}
|
|
1814
2465
|
if (normalized.mountPath) {
|
|
1815
2466
|
mountPaths.set(normalized.mountPath, key);
|
|
@@ -1817,7 +2468,9 @@ function normalizeResources(resources) {
|
|
|
1817
2468
|
const identity = resourceIdentityKey(normalized);
|
|
1818
2469
|
const seenIdentity = identities.get(identity);
|
|
1819
2470
|
if (seenIdentity && seenIdentity !== key) {
|
|
1820
|
-
throw new
|
|
2471
|
+
throw new HTTPException8(422, {
|
|
2472
|
+
message: `duplicate resource with different settings: ${identity}`
|
|
2473
|
+
});
|
|
1821
2474
|
}
|
|
1822
2475
|
identities.set(identity, key);
|
|
1823
2476
|
if (!seenResources.has(key)) {
|
|
@@ -1832,7 +2485,7 @@ function mergeResourceRefs(existing, additions) {
|
|
|
1832
2485
|
return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
|
|
1833
2486
|
} catch (error) {
|
|
1834
2487
|
if (error instanceof ResourceRefConflictError) {
|
|
1835
|
-
throw new
|
|
2488
|
+
throw new HTTPException8(422, { message: error.message });
|
|
1836
2489
|
}
|
|
1837
2490
|
throw error;
|
|
1838
2491
|
}
|
|
@@ -1842,8 +2495,8 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1842
2495
|
if (resource.kind !== "repository") {
|
|
1843
2496
|
return [];
|
|
1844
2497
|
}
|
|
1845
|
-
const installationRaw = resource.githubInstallationId;
|
|
1846
|
-
const repositoryRaw = resource.githubRepositoryId;
|
|
2498
|
+
const installationRaw = resource.githubInstallationId ?? (resource.provider === "github" ? resource.installationId : void 0);
|
|
2499
|
+
const repositoryRaw = resource.githubRepositoryId ?? (resource.provider === "github" ? resource.repositoryId : void 0);
|
|
1847
2500
|
if (installationRaw === null && repositoryRaw === null) {
|
|
1848
2501
|
return [];
|
|
1849
2502
|
}
|
|
@@ -1853,7 +2506,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1853
2506
|
const installationId2 = positiveInteger(installationRaw);
|
|
1854
2507
|
const repositoryId = positiveInteger(repositoryRaw);
|
|
1855
2508
|
if (!installationId2 || !repositoryId) {
|
|
1856
|
-
throw new
|
|
2509
|
+
throw new HTTPException8(422, {
|
|
1857
2510
|
message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
|
|
1858
2511
|
});
|
|
1859
2512
|
}
|
|
@@ -1864,7 +2517,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1864
2517
|
}
|
|
1865
2518
|
const installationId = selected[0].installationId;
|
|
1866
2519
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1867
|
-
throw new
|
|
2520
|
+
throw new HTTPException8(422, {
|
|
1868
2521
|
message: "GitHub App repository resources must belong to one installation"
|
|
1869
2522
|
});
|
|
1870
2523
|
}
|
|
@@ -1875,9 +2528,11 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
|
|
|
1875
2528
|
if (installationId === null) {
|
|
1876
2529
|
return;
|
|
1877
2530
|
}
|
|
1878
|
-
const linkedInstallationIds = new Set(
|
|
2531
|
+
const linkedInstallationIds = new Set(
|
|
2532
|
+
await listGitHubInstallationIdsForWorkspace(db, workspaceId)
|
|
2533
|
+
);
|
|
1879
2534
|
if (!linkedInstallationIds.has(installationId)) {
|
|
1880
|
-
throw new
|
|
2535
|
+
throw new HTTPException8(422, {
|
|
1881
2536
|
message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
|
|
1882
2537
|
});
|
|
1883
2538
|
}
|
|
@@ -1889,22 +2544,24 @@ async function validateFileResources(db, workspaceId, resources) {
|
|
|
1889
2544
|
continue;
|
|
1890
2545
|
}
|
|
1891
2546
|
if (fileIds.has(resource.fileId)) {
|
|
1892
|
-
throw new
|
|
2547
|
+
throw new HTTPException8(422, { message: `duplicate file resource: ${resource.fileId}` });
|
|
1893
2548
|
}
|
|
1894
2549
|
fileIds.add(resource.fileId);
|
|
1895
2550
|
const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
|
|
1896
2551
|
if (!file) {
|
|
1897
|
-
throw new
|
|
2552
|
+
throw new HTTPException8(422, { message: `unknown file resource: ${resource.fileId}` });
|
|
1898
2553
|
}
|
|
1899
2554
|
if (file.status !== "ready") {
|
|
1900
|
-
throw new
|
|
2555
|
+
throw new HTTPException8(422, {
|
|
2556
|
+
message: `file resource ${resource.fileId} is ${file.status}`
|
|
2557
|
+
});
|
|
1901
2558
|
}
|
|
1902
2559
|
}
|
|
1903
2560
|
}
|
|
1904
2561
|
function normalizeMountPath(path) {
|
|
1905
2562
|
const normalized = path.trim().replace(/^\/+|\/+$/g, "");
|
|
1906
2563
|
if (!normalized || normalized.includes("..")) {
|
|
1907
|
-
throw new
|
|
2564
|
+
throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
|
|
1908
2565
|
}
|
|
1909
2566
|
return normalized;
|
|
1910
2567
|
}
|
|
@@ -1912,7 +2569,7 @@ function parseResourceUrl(uri) {
|
|
|
1912
2569
|
try {
|
|
1913
2570
|
return new URL(uri);
|
|
1914
2571
|
} catch {
|
|
1915
|
-
throw new
|
|
2572
|
+
throw new HTTPException8(422, { message: "repository resources must use valid URLs" });
|
|
1916
2573
|
}
|
|
1917
2574
|
}
|
|
1918
2575
|
function positiveInteger(value) {
|
|
@@ -1929,38 +2586,52 @@ function positiveInteger(value) {
|
|
|
1929
2586
|
import {
|
|
1930
2587
|
createScheduledTask,
|
|
1931
2588
|
deleteScheduledTask,
|
|
2589
|
+
getRig as getRig3,
|
|
1932
2590
|
getScheduledTask,
|
|
1933
2591
|
updateScheduledTask
|
|
1934
2592
|
} from "@opengeni/db";
|
|
1935
|
-
import { HTTPException as
|
|
2593
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
1936
2594
|
|
|
1937
2595
|
// src/domain/sessions.ts
|
|
1938
2596
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
1939
|
-
import { configuredAllowedModels } from "@opengeni/config";
|
|
2597
|
+
import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
|
|
1940
2598
|
import {
|
|
1941
2599
|
CreateSessionRequest,
|
|
2600
|
+
evaluateWorkspaceModelPolicy,
|
|
1942
2601
|
reasoningEffortForMetadata
|
|
1943
2602
|
} from "@opengeni/contracts";
|
|
1944
2603
|
import {
|
|
1945
|
-
appendSessionEventsWithLockedSessionUpdate,
|
|
1946
2604
|
createSession,
|
|
1947
|
-
createSessionGoal,
|
|
1948
2605
|
createSessionWithIdempotencyKey,
|
|
1949
|
-
|
|
1950
|
-
encryptEnvironmentValue as encryptEnvironmentValue2,
|
|
2606
|
+
encryptVariableSetValue as encryptVariableSetValue2,
|
|
1951
2607
|
getAnySessionInGroup,
|
|
1952
2608
|
getEnrollment as getEnrollment2,
|
|
1953
|
-
|
|
2609
|
+
getRig as getRig2,
|
|
2610
|
+
getWorkspaceDefaultRigId,
|
|
2611
|
+
listDistinctVariableSetIdsInGroup,
|
|
2612
|
+
listDistinctRigVersionIdsInGroup,
|
|
1954
2613
|
getSandbox as getSandbox3,
|
|
1955
2614
|
getSession,
|
|
1956
2615
|
getSessionByCreateIdempotencyKey,
|
|
2616
|
+
getSessionEvent,
|
|
2617
|
+
getWorkspaceControlEvent,
|
|
2618
|
+
getSessionLineage,
|
|
1957
2619
|
getSessionTurn,
|
|
2620
|
+
getWorkspaceModelPolicy,
|
|
2621
|
+
initializeSessionStartAtomically,
|
|
1958
2622
|
requireSession as requireSession2,
|
|
1959
|
-
|
|
1960
|
-
updateSessionTitle as updateSessionTitleRow
|
|
2623
|
+
submitHumanPromptInTransaction,
|
|
2624
|
+
updateSessionTitle as updateSessionTitleRow,
|
|
2625
|
+
withWorkspaceSubjectRls,
|
|
2626
|
+
QueueCommandConflictError,
|
|
2627
|
+
SessionControlConflictError
|
|
1961
2628
|
} from "@opengeni/db";
|
|
1962
|
-
import {
|
|
1963
|
-
|
|
2629
|
+
import {
|
|
2630
|
+
appendAndPublishEvents,
|
|
2631
|
+
publishDurableSessionEvents,
|
|
2632
|
+
publishDurableWorkspaceControlEvent
|
|
2633
|
+
} from "@opengeni/events";
|
|
2634
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
1964
2635
|
var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
|
|
1965
2636
|
var maxSessionMcpCredentialHeaders = 16;
|
|
1966
2637
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
@@ -1971,23 +2642,29 @@ function normalizedSessionMcpCredentialHeaders(headers) {
|
|
|
1971
2642
|
}
|
|
1972
2643
|
const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
|
|
1973
2644
|
if (entries.length > maxSessionMcpCredentialHeaders) {
|
|
1974
|
-
throw new
|
|
2645
|
+
throw new HTTPException9(422, {
|
|
2646
|
+
message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
|
|
2647
|
+
});
|
|
1975
2648
|
}
|
|
1976
2649
|
const seen = /* @__PURE__ */ new Set();
|
|
1977
2650
|
for (const [name, value] of entries) {
|
|
1978
2651
|
if (!sessionMcpCredentialHeaderName.test(name)) {
|
|
1979
|
-
throw new
|
|
2652
|
+
throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
|
|
1980
2653
|
}
|
|
1981
2654
|
const lower = name.toLowerCase();
|
|
1982
2655
|
if (seen.has(lower)) {
|
|
1983
|
-
throw new
|
|
2656
|
+
throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
|
|
1984
2657
|
}
|
|
1985
2658
|
seen.add(lower);
|
|
1986
2659
|
if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
|
|
1987
|
-
throw new
|
|
2660
|
+
throw new HTTPException9(422, {
|
|
2661
|
+
message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
|
|
2662
|
+
});
|
|
1988
2663
|
}
|
|
1989
2664
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
1990
|
-
throw new
|
|
2665
|
+
throw new HTTPException9(422, {
|
|
2666
|
+
message: `credential header ${name} contains forbidden control characters`
|
|
2667
|
+
});
|
|
1991
2668
|
}
|
|
1992
2669
|
}
|
|
1993
2670
|
return Object.fromEntries(entries);
|
|
@@ -2018,10 +2695,7 @@ function settingsWithSessionMcpServerConfigs(settings, servers) {
|
|
|
2018
2695
|
const sessionIds = new Set(servers.map((server) => server.id));
|
|
2019
2696
|
return {
|
|
2020
2697
|
...settings,
|
|
2021
|
-
mcpServers: [
|
|
2022
|
-
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
2023
|
-
...servers
|
|
2024
|
-
]
|
|
2698
|
+
mcpServers: [...settings.mcpServers.filter((server) => !sessionIds.has(server.id)), ...servers]
|
|
2025
2699
|
};
|
|
2026
2700
|
}
|
|
2027
2701
|
function settingsWithSessionMcpServerMetadata(settings, servers) {
|
|
@@ -2032,7 +2706,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2032
2706
|
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
2033
2707
|
}
|
|
2034
2708
|
requirePermission(grant, "mcp_servers:attach");
|
|
2035
|
-
const encryptionKey =
|
|
2709
|
+
const encryptionKey = requireVariableSetEncryption(settings);
|
|
2036
2710
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
2037
2711
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2038
2712
|
const runtimeServers = [];
|
|
@@ -2040,15 +2714,18 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2040
2714
|
const metadata = [];
|
|
2041
2715
|
for (const server of servers) {
|
|
2042
2716
|
if (seenIds.has(server.id)) {
|
|
2043
|
-
throw new
|
|
2717
|
+
throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
|
|
2044
2718
|
}
|
|
2045
2719
|
seenIds.add(server.id);
|
|
2046
2720
|
if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
|
|
2047
|
-
throw new
|
|
2721
|
+
throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
|
|
2048
2722
|
}
|
|
2049
2723
|
const headers = normalizedSessionMcpCredentialHeaders(server.headers);
|
|
2050
2724
|
const headersEncrypted = Object.fromEntries(
|
|
2051
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2725
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2726
|
+
name,
|
|
2727
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2728
|
+
])
|
|
2052
2729
|
);
|
|
2053
2730
|
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
2054
2731
|
dbServers.push({
|
|
@@ -2076,22 +2753,27 @@ function validateSessionMcpCredentialUpdates(input) {
|
|
|
2076
2753
|
return [];
|
|
2077
2754
|
}
|
|
2078
2755
|
requirePermission(input.grant, "mcp_servers:attach");
|
|
2079
|
-
const encryptionKey =
|
|
2756
|
+
const encryptionKey = requireVariableSetEncryption(input.settings);
|
|
2080
2757
|
const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
|
|
2081
2758
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2082
2759
|
const encryptedUpdates = input.updates.map((update) => {
|
|
2083
2760
|
if (seenIds.has(update.id)) {
|
|
2084
|
-
throw new
|
|
2761
|
+
throw new HTTPException9(422, {
|
|
2762
|
+
message: `duplicate session MCP credential update id: ${update.id}`
|
|
2763
|
+
});
|
|
2085
2764
|
}
|
|
2086
2765
|
seenIds.add(update.id);
|
|
2087
2766
|
if (!knownIds.has(update.id)) {
|
|
2088
|
-
throw new
|
|
2767
|
+
throw new HTTPException9(422, { message: `unknown session MCP server id: ${update.id}` });
|
|
2089
2768
|
}
|
|
2090
2769
|
const headers = normalizedSessionMcpCredentialHeaders(update.headers);
|
|
2091
2770
|
return {
|
|
2092
2771
|
id: update.id,
|
|
2093
2772
|
headersEncrypted: Object.fromEntries(
|
|
2094
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2773
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2774
|
+
name,
|
|
2775
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2776
|
+
])
|
|
2095
2777
|
)
|
|
2096
2778
|
};
|
|
2097
2779
|
});
|
|
@@ -2104,9 +2786,16 @@ async function createAndStartSession(input) {
|
|
|
2104
2786
|
reasoningEffort: input.reasoningEffort
|
|
2105
2787
|
};
|
|
2106
2788
|
if (input.createIdempotencyKey) {
|
|
2107
|
-
const existing = await getSessionByCreateIdempotencyKey(
|
|
2789
|
+
const existing = await getSessionByCreateIdempotencyKey(
|
|
2790
|
+
input.db,
|
|
2791
|
+
input.workspaceId,
|
|
2792
|
+
input.createIdempotencyKey
|
|
2793
|
+
);
|
|
2108
2794
|
if (existing) {
|
|
2109
|
-
return
|
|
2795
|
+
return await finishStartSession(
|
|
2796
|
+
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2797
|
+
existing
|
|
2798
|
+
);
|
|
2110
2799
|
}
|
|
2111
2800
|
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
2112
2801
|
accountId: input.accountId,
|
|
@@ -2117,7 +2806,9 @@ async function createAndStartSession(input) {
|
|
|
2117
2806
|
metadata: sessionMetadata,
|
|
2118
2807
|
model: input.model,
|
|
2119
2808
|
sandboxBackend: input.sandboxBackend,
|
|
2120
|
-
|
|
2809
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2810
|
+
rigId: input.rigId ?? null,
|
|
2811
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2121
2812
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2122
2813
|
instructions: input.instructions ?? null,
|
|
2123
2814
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2127,7 +2818,10 @@ async function createAndStartSession(input) {
|
|
|
2127
2818
|
mcpServers: input.mcpServers ?? []
|
|
2128
2819
|
});
|
|
2129
2820
|
if (!created) {
|
|
2130
|
-
return
|
|
2821
|
+
return await finishStartSession(
|
|
2822
|
+
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2823
|
+
keyed
|
|
2824
|
+
);
|
|
2131
2825
|
}
|
|
2132
2826
|
return await finishStartSession(input, keyed);
|
|
2133
2827
|
}
|
|
@@ -2140,7 +2834,9 @@ async function createAndStartSession(input) {
|
|
|
2140
2834
|
metadata: sessionMetadata,
|
|
2141
2835
|
model: input.model,
|
|
2142
2836
|
sandboxBackend: input.sandboxBackend,
|
|
2143
|
-
|
|
2837
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2838
|
+
rigId: input.rigId ?? null,
|
|
2839
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2144
2840
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2145
2841
|
instructions: input.instructions ?? null,
|
|
2146
2842
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2151,54 +2847,9 @@ async function createAndStartSession(input) {
|
|
|
2151
2847
|
return await finishStartSession(input, session);
|
|
2152
2848
|
}
|
|
2153
2849
|
async function finishStartSession(input, session) {
|
|
2154
|
-
const goal = input.goal ? await createSessionGoal(input.db, {
|
|
2155
|
-
accountId: session.accountId,
|
|
2156
|
-
workspaceId: session.workspaceId,
|
|
2157
|
-
sessionId: session.id,
|
|
2158
|
-
text: input.goal.text,
|
|
2159
|
-
successCriteria: input.goal.successCriteria ?? null,
|
|
2160
|
-
maxAutoContinuations: input.goal.maxAutoContinuations ?? null,
|
|
2161
|
-
createdBy: "api"
|
|
2162
|
-
}) : null;
|
|
2163
|
-
const initialPayload = {
|
|
2164
|
-
text: input.initialMessage,
|
|
2165
|
-
...input.resources.length ? { resources: input.resources } : {},
|
|
2166
|
-
...input.tools.length ? { tools: input.tools } : {}
|
|
2167
|
-
};
|
|
2168
|
-
const events = await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [
|
|
2169
|
-
{
|
|
2170
|
-
type: "session.created",
|
|
2171
|
-
payload: {
|
|
2172
|
-
status: "queued",
|
|
2173
|
-
...input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {},
|
|
2174
|
-
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
2175
|
-
}
|
|
2176
|
-
},
|
|
2177
|
-
...goal ? [{
|
|
2178
|
-
type: "goal.set",
|
|
2179
|
-
payload: {
|
|
2180
|
-
goalId: goal.id,
|
|
2181
|
-
text: goal.text,
|
|
2182
|
-
...goal.successCriteria ? { successCriteria: goal.successCriteria } : {},
|
|
2183
|
-
version: goal.version,
|
|
2184
|
-
actor: "api",
|
|
2185
|
-
replaced: false
|
|
2186
|
-
}
|
|
2187
|
-
}] : [],
|
|
2188
|
-
{
|
|
2189
|
-
type: "user.message",
|
|
2190
|
-
payload: initialPayload,
|
|
2191
|
-
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2192
|
-
},
|
|
2193
|
-
{ type: "session.status.changed", payload: { status: "queued" } }
|
|
2194
|
-
]);
|
|
2195
|
-
const userEvent = events.find((event) => event.type === "user.message");
|
|
2196
|
-
if (!userEvent) {
|
|
2197
|
-
throw new HTTPException8(500, { message: "failed to append initial user event" });
|
|
2198
|
-
}
|
|
2199
2850
|
if (input.seedTargetSandbox) {
|
|
2200
2851
|
if (session.sandboxBackend === "none") {
|
|
2201
|
-
throw new
|
|
2852
|
+
throw new HTTPException9(422, {
|
|
2202
2853
|
message: "cannot target a machine for a session with no sandbox (backend: none)"
|
|
2203
2854
|
});
|
|
2204
2855
|
}
|
|
@@ -2218,34 +2869,37 @@ async function finishStartSession(input, session) {
|
|
|
2218
2869
|
input.seedTargetSandbox.workingDir ?? null
|
|
2219
2870
|
);
|
|
2220
2871
|
if (!seeded.swapped) {
|
|
2221
|
-
throw new
|
|
2872
|
+
throw new HTTPException9(422, {
|
|
2222
2873
|
message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
|
|
2223
2874
|
});
|
|
2224
2875
|
}
|
|
2225
2876
|
}
|
|
2226
|
-
const
|
|
2227
|
-
await setTemporalWorkflowId(input.db, session.workspaceId, session.id, workflowId);
|
|
2228
|
-
const turn = await enqueueSessionTurn(input.db, {
|
|
2877
|
+
const started = await initializeSessionStartAtomically(input.db, {
|
|
2229
2878
|
accountId: session.accountId,
|
|
2230
2879
|
workspaceId: session.workspaceId,
|
|
2231
2880
|
sessionId: session.id,
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2881
|
+
...input.clientEventId ? { clientEventId: input.clientEventId } : {},
|
|
2882
|
+
reasoningEffortFallback: input.reasoningEffort,
|
|
2883
|
+
createdEventPayload: {
|
|
2884
|
+
...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
|
|
2885
|
+
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
2886
|
+
},
|
|
2887
|
+
goal: input.goal ? {
|
|
2888
|
+
text: input.goal.text,
|
|
2889
|
+
...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
|
|
2890
|
+
...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
|
|
2891
|
+
} : null
|
|
2242
2892
|
});
|
|
2243
|
-
await
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2893
|
+
await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
|
|
2894
|
+
if (started.workflowWakeRevision !== null) {
|
|
2895
|
+
await input.workflowClient.wakeSessionWorkflow({
|
|
2896
|
+
accountId: session.accountId,
|
|
2897
|
+
workspaceId: session.workspaceId,
|
|
2898
|
+
sessionId: session.id,
|
|
2899
|
+
workflowId: started.temporalWorkflowId,
|
|
2900
|
+
wakeRevision: started.workflowWakeRevision
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2249
2903
|
return await requireSession2(input.db, session.workspaceId, session.id);
|
|
2250
2904
|
}
|
|
2251
2905
|
function workflowIdForSession(sessionId) {
|
|
@@ -2261,15 +2915,33 @@ function assertConfiguredModel(settings, model) {
|
|
|
2261
2915
|
if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
2262
2916
|
return;
|
|
2263
2917
|
}
|
|
2264
|
-
throw new
|
|
2918
|
+
throw new HTTPException9(422, { message: `model is not available: ${model}` });
|
|
2919
|
+
}
|
|
2920
|
+
async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
|
|
2921
|
+
if (model === null || model === void 0) {
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
2925
|
+
if (!policy) {
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
const providerId = policyProviderIdForModel(settings, model);
|
|
2929
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, { providerId, modelId: model });
|
|
2930
|
+
if (!verdict.allowed) {
|
|
2931
|
+
throw new HTTPException9(422, {
|
|
2932
|
+
message: verdict.reason === "provider" ? `model "${model}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${model}" is not allowed by this workspace's model policy`
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2265
2935
|
}
|
|
2266
2936
|
async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
|
|
2267
2937
|
const turn = await getSessionTurn(db, workspaceId, turnId);
|
|
2268
2938
|
if (!turn || turn.sessionId !== sessionId) {
|
|
2269
|
-
throw new
|
|
2939
|
+
throw new HTTPException9(404, { message: "session turn not found" });
|
|
2270
2940
|
}
|
|
2271
2941
|
if (turn.status !== "queued") {
|
|
2272
|
-
throw new
|
|
2942
|
+
throw new HTTPException9(409, {
|
|
2943
|
+
message: `turn is ${turn.status}; only queued turns can be changed`
|
|
2944
|
+
});
|
|
2273
2945
|
}
|
|
2274
2946
|
return turn;
|
|
2275
2947
|
}
|
|
@@ -2281,98 +2953,162 @@ async function postUserMessageTurn(input) {
|
|
|
2281
2953
|
const requestedModel = input.model ?? null;
|
|
2282
2954
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
2283
2955
|
assertConfiguredModel(settings, requestedModel);
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2956
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
2957
|
+
const operationKey = input.clientEventId ?? crypto.randomUUID();
|
|
2958
|
+
let result;
|
|
2959
|
+
try {
|
|
2960
|
+
result = await withWorkspaceSubjectRls(
|
|
2961
|
+
db,
|
|
2962
|
+
workspaceId,
|
|
2963
|
+
input.actor ?? accountId,
|
|
2964
|
+
(scoped) => scoped.transaction(
|
|
2965
|
+
(tx) => submitHumanPromptInTransaction(tx, {
|
|
2966
|
+
accountId,
|
|
2967
|
+
workspaceId,
|
|
2968
|
+
sessionId,
|
|
2969
|
+
subjectId: input.actor ?? accountId,
|
|
2970
|
+
actor: { type: "human", subjectId: input.actor ?? accountId },
|
|
2971
|
+
operationKey,
|
|
2972
|
+
delivery: input.delivery ?? "send",
|
|
2973
|
+
controlEtag: input.controlEtag ?? null,
|
|
2974
|
+
expectedDraftRevision: input.expectedDraftRevision ?? null,
|
|
2975
|
+
text: input.text,
|
|
2976
|
+
resources: input.resources,
|
|
2977
|
+
tools: input.tools,
|
|
2978
|
+
model: requestedModel,
|
|
2979
|
+
reasoningEffort: requestedReasoningEffort,
|
|
2980
|
+
reasoningEffortFallback: settings.openaiReasoningEffort,
|
|
2981
|
+
source: input.origin === "operator" ? "api" : "user",
|
|
2982
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? []
|
|
2983
|
+
})
|
|
2984
|
+
)
|
|
2985
|
+
);
|
|
2986
|
+
} catch (error) {
|
|
2987
|
+
if (error instanceof QueueCommandConflictError || error instanceof SessionControlConflictError) {
|
|
2988
|
+
throw new HTTPException9(409, { message: error.message });
|
|
2287
2989
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
throw new HTTPException8(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
|
|
2990
|
+
if (error instanceof Error && error.message.includes("cancelled")) {
|
|
2991
|
+
throw new HTTPException9(409, { message: error.message });
|
|
2291
2992
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
...shouldQueueSession ? [{ type: "session.status.changed", payload: { status: "queued" } }] : []
|
|
2310
|
-
],
|
|
2311
|
-
update: {
|
|
2312
|
-
resources: nextResources,
|
|
2313
|
-
tools: nextTools,
|
|
2314
|
-
...shouldQueueSession ? { status: "queued", activeTurnId: null } : {}
|
|
2315
|
-
}
|
|
2316
|
-
};
|
|
2317
|
-
}).then(async (events) => {
|
|
2318
|
-
await bus.publish(workspaceId, sessionId, events);
|
|
2319
|
-
return events;
|
|
2320
|
-
});
|
|
2321
|
-
const accepted = appended[0];
|
|
2322
|
-
if (!accepted) {
|
|
2323
|
-
throw new HTTPException8(500, { message: "failed to append client event" });
|
|
2324
|
-
}
|
|
2325
|
-
const workflowId = workflowIdForSession(sessionId);
|
|
2326
|
-
const session = await requireSession2(db, workspaceId, sessionId);
|
|
2327
|
-
const turn = await enqueueSessionTurn(db, {
|
|
2328
|
-
accountId,
|
|
2993
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
2994
|
+
throw new HTTPException9(422, { message: error.message });
|
|
2995
|
+
}
|
|
2996
|
+
throw error;
|
|
2997
|
+
}
|
|
2998
|
+
const events = await Promise.all(
|
|
2999
|
+
result.eventIds.map((eventId) => getSessionEvent(db, workspaceId, eventId))
|
|
3000
|
+
);
|
|
3001
|
+
if (events.some((event) => event === null)) {
|
|
3002
|
+
throw new Error("Committed prompt events could not be reloaded");
|
|
3003
|
+
}
|
|
3004
|
+
const turn = await getSessionTurn(db, workspaceId, result.turnId);
|
|
3005
|
+
if (!turn) throw new Error("Committed prompt turn could not be reloaded");
|
|
3006
|
+
const accepted = events.find((event) => event?.id === result.acceptedEventId);
|
|
3007
|
+
if (!accepted) throw new Error("Committed user.message event could not be reloaded");
|
|
3008
|
+
await publishDurableSessionEvents(
|
|
3009
|
+
bus,
|
|
2329
3010
|
workspaceId,
|
|
2330
3011
|
sessionId,
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
3012
|
+
events.filter((event) => event !== null)
|
|
3013
|
+
);
|
|
3014
|
+
if (result.workspaceControlEventId) {
|
|
3015
|
+
const controlEvent = await getWorkspaceControlEvent(
|
|
3016
|
+
db,
|
|
3017
|
+
workspaceId,
|
|
3018
|
+
result.workspaceControlEventId
|
|
3019
|
+
);
|
|
3020
|
+
if (!controlEvent) {
|
|
3021
|
+
throw new Error(
|
|
3022
|
+
`Committed workspace control event disappeared: ${result.workspaceControlEventId}`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
await publishDurableWorkspaceControlEvent(bus, workspaceId, controlEvent);
|
|
3026
|
+
}
|
|
3027
|
+
try {
|
|
3028
|
+
await workflowClient.wakeSessionWorkflow({
|
|
3029
|
+
accountId,
|
|
3030
|
+
workspaceId,
|
|
3031
|
+
sessionId,
|
|
3032
|
+
workflowId: turn.temporalWorkflowId,
|
|
3033
|
+
wakeRevision: result.wakeRevision,
|
|
3034
|
+
...result.interruptionCount > 0 ? { interruptionRequested: true } : {}
|
|
3035
|
+
});
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
console.warn(
|
|
3038
|
+
`[sessions] workflow wake failed for committed prompt ${workspaceId}/${sessionId}; durable outbox will retry`,
|
|
3039
|
+
error
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
2348
3042
|
return { accepted, turn };
|
|
2349
3043
|
}
|
|
2350
3044
|
async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
2351
3045
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2352
3046
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
2353
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
2354
|
-
|
|
2355
|
-
|
|
3047
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3048
|
+
db,
|
|
3049
|
+
workspaceId,
|
|
3050
|
+
settings
|
|
3051
|
+
);
|
|
3052
|
+
const sessionMcpServers = validateSessionMcpServersForCreate(
|
|
3053
|
+
capabilityRuntimeSettings,
|
|
3054
|
+
grant,
|
|
3055
|
+
payload.mcpServers
|
|
3056
|
+
);
|
|
3057
|
+
const runtimeSettings = settingsWithSessionMcpServerConfigs(
|
|
3058
|
+
capabilityRuntimeSettings,
|
|
3059
|
+
sessionMcpServers.runtimeServers
|
|
3060
|
+
);
|
|
2356
3061
|
const resources = normalizeResources(payload.resources);
|
|
2357
3062
|
const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
|
|
2358
3063
|
const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
2359
3064
|
const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
|
|
2360
3065
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
2361
3066
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2362
|
-
throw new
|
|
3067
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2363
3068
|
}
|
|
2364
3069
|
await validateFileResources(db, workspaceId, resources);
|
|
2365
|
-
const
|
|
3070
|
+
const variableSet = payload.variableSetId ? await validateVariableSetAttachment(
|
|
3071
|
+
{ settings, db },
|
|
3072
|
+
grant,
|
|
3073
|
+
workspaceId,
|
|
3074
|
+
payload.variableSetId
|
|
3075
|
+
) : null;
|
|
3076
|
+
const requestedRigId = payload.rigId ?? await getWorkspaceDefaultRigId(db, workspaceId);
|
|
3077
|
+
let frozenRigId = null;
|
|
3078
|
+
let frozenRigVersionId = null;
|
|
3079
|
+
if (requestedRigId) {
|
|
3080
|
+
const rig = await getRig2(db, workspaceId, requestedRigId);
|
|
3081
|
+
if (!rig || !rig.activeVersion) {
|
|
3082
|
+
if (payload.rigId) {
|
|
3083
|
+
throw new HTTPException9(422, {
|
|
3084
|
+
message: rig ? `rig ${payload.rigId} has no active version to bind` : `unknown rigId: ${payload.rigId}`
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
} else {
|
|
3088
|
+
frozenRigId = rig.id;
|
|
3089
|
+
frozenRigVersionId = rig.activeVersion.id;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
2366
3092
|
assertConfiguredModel(settings, payload.model);
|
|
3093
|
+
await assertWorkspaceModelPolicyAllows(
|
|
3094
|
+
db,
|
|
3095
|
+
settings,
|
|
3096
|
+
workspaceId,
|
|
3097
|
+
payload.model ?? settings.openaiModel
|
|
3098
|
+
);
|
|
2367
3099
|
const model = payload.model ?? settings.openaiModel;
|
|
2368
3100
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
2369
3101
|
let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
|
|
2370
3102
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
2371
|
-
throw new
|
|
3103
|
+
throw new HTTPException9(422, {
|
|
3104
|
+
message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
|
|
3105
|
+
});
|
|
2372
3106
|
}
|
|
2373
3107
|
for (const permission of firstPartyMcpPermissions ?? []) {
|
|
2374
3108
|
if (!hasPermission(grant.permissions, permission)) {
|
|
2375
|
-
throw new
|
|
3109
|
+
throw new HTTPException9(403, {
|
|
3110
|
+
message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
|
|
3111
|
+
});
|
|
2376
3112
|
}
|
|
2377
3113
|
}
|
|
2378
3114
|
if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
|
|
@@ -2382,19 +3118,39 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2382
3118
|
const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
|
|
2383
3119
|
let sandboxGroupId = null;
|
|
2384
3120
|
let inheritedBackend;
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
3121
|
+
const requestedVariableSetId = payload.variableSetId ?? null;
|
|
3122
|
+
const variableSetMatchesGroup = (memberVariableSetId) => memberVariableSetId === requestedVariableSetId;
|
|
3123
|
+
const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
|
|
2387
3124
|
if (sandboxChoice === "shared") {
|
|
2388
3125
|
if (!parentSessionId) {
|
|
2389
|
-
throw new
|
|
3126
|
+
throw new HTTPException9(422, {
|
|
3127
|
+
message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
|
|
3128
|
+
});
|
|
2390
3129
|
}
|
|
2391
3130
|
const parent = await getSession(db, workspaceId, parentSessionId);
|
|
2392
3131
|
if (!parent) {
|
|
2393
|
-
throw new
|
|
3132
|
+
throw new HTTPException9(404, {
|
|
3133
|
+
message: `parent session not found in workspace: ${parentSessionId}`
|
|
3134
|
+
});
|
|
2394
3135
|
}
|
|
2395
|
-
|
|
3136
|
+
const parentBoxed = parent.sandboxBackend !== "none";
|
|
3137
|
+
const variableSetMismatch = parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
|
|
3138
|
+
let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
|
|
3139
|
+
if (parentBoxed && !rigMismatch) {
|
|
3140
|
+
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
3141
|
+
db,
|
|
3142
|
+
workspaceId,
|
|
3143
|
+
parent.sandboxGroupId
|
|
3144
|
+
);
|
|
3145
|
+
rigMismatch = !memberRigVersionIds.every(
|
|
3146
|
+
(memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
if (variableSetMismatch || rigMismatch) {
|
|
2396
3150
|
if (payload.sandbox === "shared") {
|
|
2397
|
-
throw new
|
|
3151
|
+
throw new HTTPException9(422, {
|
|
3152
|
+
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."
|
|
3153
|
+
});
|
|
2398
3154
|
}
|
|
2399
3155
|
} else {
|
|
2400
3156
|
sandboxGroupId = parent.sandboxGroupId;
|
|
@@ -2403,19 +3159,43 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2403
3159
|
} else if (typeof sandboxChoice === "object") {
|
|
2404
3160
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
2405
3161
|
if (!member) {
|
|
2406
|
-
throw new
|
|
3162
|
+
throw new HTTPException9(404, {
|
|
3163
|
+
message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
|
|
3164
|
+
});
|
|
2407
3165
|
}
|
|
2408
3166
|
if (member.sandboxBackend !== "none") {
|
|
2409
|
-
const
|
|
2410
|
-
|
|
2411
|
-
|
|
3167
|
+
const memberVariableSetIds = await listDistinctVariableSetIdsInGroup(
|
|
3168
|
+
db,
|
|
3169
|
+
workspaceId,
|
|
3170
|
+
sandboxChoice.groupId
|
|
3171
|
+
);
|
|
3172
|
+
if (!memberVariableSetIds.every(
|
|
3173
|
+
(memberVariableSetId) => variableSetMatchesGroup(memberVariableSetId)
|
|
3174
|
+
)) {
|
|
3175
|
+
throw new HTTPException9(422, {
|
|
3176
|
+
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.`
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
3180
|
+
db,
|
|
3181
|
+
workspaceId,
|
|
3182
|
+
sandboxChoice.groupId
|
|
3183
|
+
);
|
|
3184
|
+
if (!memberRigVersionIds.every(
|
|
3185
|
+
(memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
|
|
3186
|
+
)) {
|
|
3187
|
+
throw new HTTPException9(422, {
|
|
3188
|
+
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.`
|
|
3189
|
+
});
|
|
2412
3190
|
}
|
|
2413
3191
|
}
|
|
2414
3192
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2415
3193
|
inheritedBackend = member.sandboxBackend;
|
|
2416
3194
|
}
|
|
2417
3195
|
if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
|
|
2418
|
-
throw new
|
|
3196
|
+
throw new HTTPException9(422, {
|
|
3197
|
+
message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
|
|
3198
|
+
});
|
|
2419
3199
|
}
|
|
2420
3200
|
let machineHomeBackend;
|
|
2421
3201
|
let machineHomeOs;
|
|
@@ -2431,7 +3211,13 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2431
3211
|
}
|
|
2432
3212
|
}
|
|
2433
3213
|
}
|
|
2434
|
-
await requireLimit(deps, {
|
|
3214
|
+
await requireLimit(deps, {
|
|
3215
|
+
accountId: grant.accountId,
|
|
3216
|
+
workspaceId,
|
|
3217
|
+
action: "agent_run:create",
|
|
3218
|
+
quantity: 1,
|
|
3219
|
+
model
|
|
3220
|
+
});
|
|
2435
3221
|
const session = await createAndStartSession({
|
|
2436
3222
|
db,
|
|
2437
3223
|
bus,
|
|
@@ -2456,7 +3242,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2456
3242
|
...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
|
|
2457
3243
|
sandboxGroupId,
|
|
2458
3244
|
metadata: payload.metadata,
|
|
2459
|
-
|
|
3245
|
+
variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
|
|
3246
|
+
// Frozen rig binding (M3): both null for a rig-less session (today's path).
|
|
3247
|
+
rigId: frozenRigId,
|
|
3248
|
+
rigVersionId: frozenRigVersionId,
|
|
2460
3249
|
goal: payload.goal ?? null,
|
|
2461
3250
|
// Per-session persona instructions (already trimmed/validated by the
|
|
2462
3251
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
@@ -2488,9 +3277,16 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2488
3277
|
}
|
|
2489
3278
|
async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
|
|
2490
3279
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2491
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3280
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3281
|
+
db,
|
|
3282
|
+
workspaceId,
|
|
3283
|
+
settings
|
|
3284
|
+
);
|
|
2492
3285
|
const existingSession = await requireSession2(db, workspaceId, sessionId);
|
|
2493
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3286
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3287
|
+
capabilityRuntimeSettings,
|
|
3288
|
+
existingSession.mcpServers
|
|
3289
|
+
);
|
|
2494
3290
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
2495
3291
|
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
2496
3292
|
const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
|
|
@@ -2502,10 +3298,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2502
3298
|
model: input.model ?? existingSession.model
|
|
2503
3299
|
});
|
|
2504
3300
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2505
|
-
throw new
|
|
3301
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2506
3302
|
}
|
|
2507
3303
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
2508
|
-
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3304
|
+
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3305
|
+
...existingSession.resources,
|
|
3306
|
+
...requestedResources
|
|
3307
|
+
]);
|
|
2509
3308
|
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
2510
3309
|
settings,
|
|
2511
3310
|
grant,
|
|
@@ -2526,6 +3325,11 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2526
3325
|
model: input.model ?? null,
|
|
2527
3326
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
2528
3327
|
mcpCredentialUpdates,
|
|
3328
|
+
delivery: input.delivery ?? "send",
|
|
3329
|
+
origin: input.origin ?? "human",
|
|
3330
|
+
actor: grant.subjectId,
|
|
3331
|
+
...input.controlEtag !== void 0 ? { controlEtag: input.controlEtag } : {},
|
|
3332
|
+
...input.expectedDraftRevision !== void 0 ? { expectedDraftRevision: input.expectedDraftRevision } : {},
|
|
2529
3333
|
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2530
3334
|
});
|
|
2531
3335
|
await recordWorkspaceUsage(deps, {
|
|
@@ -2545,16 +3349,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
|
|
|
2545
3349
|
const { db, bus } = deps;
|
|
2546
3350
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
2547
3351
|
if (result.updated) {
|
|
2548
|
-
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
3352
|
+
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
3353
|
+
{
|
|
3354
|
+
type: "session.title_set",
|
|
3355
|
+
payload: {
|
|
3356
|
+
title: result.title ?? title,
|
|
3357
|
+
source
|
|
3358
|
+
}
|
|
2553
3359
|
}
|
|
2554
|
-
|
|
3360
|
+
]);
|
|
2555
3361
|
}
|
|
2556
3362
|
return result;
|
|
2557
3363
|
}
|
|
3364
|
+
async function readSessionLineage(db, workspaceId, sessionId) {
|
|
3365
|
+
const lineage = await getSessionLineage(db, workspaceId, sessionId);
|
|
3366
|
+
if (!lineage) {
|
|
3367
|
+
throw new HTTPException9(404, { message: "session not found" });
|
|
3368
|
+
}
|
|
3369
|
+
return lineage;
|
|
3370
|
+
}
|
|
2558
3371
|
function withFirstPartyTools(tools, runtimeSettings) {
|
|
2559
3372
|
if (!runtimeSettings.mcpServers.some((server) => server.id === "opengeni")) {
|
|
2560
3373
|
return tools;
|
|
@@ -2562,7 +3375,9 @@ function withFirstPartyTools(tools, runtimeSettings) {
|
|
|
2562
3375
|
return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
|
|
2563
3376
|
}
|
|
2564
3377
|
function hasOwnProperty(value, key) {
|
|
2565
|
-
return Boolean(
|
|
3378
|
+
return Boolean(
|
|
3379
|
+
value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key)
|
|
3380
|
+
);
|
|
2566
3381
|
}
|
|
2567
3382
|
|
|
2568
3383
|
// src/domain/scheduled-tasks.ts
|
|
@@ -2576,18 +3391,24 @@ function scheduledTaskToolsProvided(rawPayload) {
|
|
|
2576
3391
|
);
|
|
2577
3392
|
}
|
|
2578
3393
|
async function createValidatedScheduledTask(input) {
|
|
2579
|
-
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3394
|
+
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3395
|
+
...input,
|
|
3396
|
+
workspaceId: input.grant.workspaceId
|
|
3397
|
+
});
|
|
2580
3398
|
const id = crypto.randomUUID();
|
|
2581
3399
|
validateScheduledTaskSchedule(input.payload.schedule);
|
|
2582
|
-
if (input.payload.
|
|
2583
|
-
await
|
|
3400
|
+
if (input.payload.variableSetId) {
|
|
3401
|
+
await validateVariableSetAttachment(
|
|
2584
3402
|
{ settings: input.settings, db: input.db },
|
|
2585
3403
|
input.grant,
|
|
2586
3404
|
input.grant.workspaceId,
|
|
2587
|
-
input.payload.
|
|
2588
|
-
{ preauthorized: input.
|
|
3405
|
+
input.payload.variableSetId,
|
|
3406
|
+
{ preauthorized: input.variableSetPreauthorized ?? false }
|
|
2589
3407
|
);
|
|
2590
3408
|
}
|
|
3409
|
+
if (input.payload.rigId) {
|
|
3410
|
+
await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
|
|
3411
|
+
}
|
|
2591
3412
|
return await createScheduledTask(input.db, {
|
|
2592
3413
|
id,
|
|
2593
3414
|
accountId: input.grant.accountId,
|
|
@@ -2599,10 +3420,17 @@ async function createValidatedScheduledTask(input) {
|
|
|
2599
3420
|
runMode: input.payload.runMode,
|
|
2600
3421
|
overlapPolicy: input.payload.overlapPolicy,
|
|
2601
3422
|
agentConfig,
|
|
2602
|
-
|
|
3423
|
+
variableSetId: input.payload.variableSetId ?? null,
|
|
3424
|
+
rigId: input.payload.rigId ?? null,
|
|
2603
3425
|
metadata: input.payload.metadata
|
|
2604
3426
|
});
|
|
2605
3427
|
}
|
|
3428
|
+
async function requireScheduledTaskRig(db, workspaceId, rigId) {
|
|
3429
|
+
const rig = await getRig3(db, workspaceId, rigId);
|
|
3430
|
+
if (!rig) {
|
|
3431
|
+
throw new HTTPException10(422, { message: `unknown rigId: ${rigId}` });
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
2606
3434
|
async function validatedScheduledTaskUpdate(input) {
|
|
2607
3435
|
const update = {};
|
|
2608
3436
|
if (input.payload.name !== void 0) {
|
|
@@ -2624,30 +3452,38 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2624
3452
|
if (input.payload.metadata !== void 0) {
|
|
2625
3453
|
update.metadata = input.payload.metadata;
|
|
2626
3454
|
}
|
|
2627
|
-
if (input.payload.
|
|
2628
|
-
const
|
|
2629
|
-
if ((input.existing.
|
|
2630
|
-
throw new
|
|
3455
|
+
if (input.payload.variableSetId !== void 0) {
|
|
3456
|
+
const nextVariableSetId = input.payload.variableSetId;
|
|
3457
|
+
if ((input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
|
|
3458
|
+
throw new HTTPException10(409, {
|
|
3459
|
+
message: "cannot change variableSet of a task with a live reusable session; recreate the task"
|
|
3460
|
+
});
|
|
2631
3461
|
}
|
|
2632
|
-
if (
|
|
2633
|
-
if (input.existing.
|
|
2634
|
-
requirePermission(input.grant, "
|
|
3462
|
+
if (nextVariableSetId === null) {
|
|
3463
|
+
if (input.existing.variableSetId !== null) {
|
|
3464
|
+
requirePermission(input.grant, "variable-sets:use");
|
|
2635
3465
|
}
|
|
2636
|
-
update.
|
|
3466
|
+
update.variableSetId = null;
|
|
2637
3467
|
} else {
|
|
2638
|
-
await
|
|
3468
|
+
await validateVariableSetAttachment(
|
|
2639
3469
|
{ settings: input.settings, db: input.db },
|
|
2640
3470
|
input.grant,
|
|
2641
3471
|
input.existing.workspaceId,
|
|
2642
|
-
|
|
3472
|
+
nextVariableSetId
|
|
2643
3473
|
);
|
|
2644
|
-
update.
|
|
3474
|
+
update.variableSetId = nextVariableSetId;
|
|
2645
3475
|
}
|
|
2646
3476
|
}
|
|
3477
|
+
if (input.payload.rigId !== void 0) {
|
|
3478
|
+
if (input.payload.rigId !== null) {
|
|
3479
|
+
await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
|
|
3480
|
+
}
|
|
3481
|
+
update.rigId = input.payload.rigId;
|
|
3482
|
+
}
|
|
2647
3483
|
if (input.payload.agentConfig !== void 0) {
|
|
2648
|
-
const
|
|
2649
|
-
if (
|
|
2650
|
-
requirePermission(input.grant, "
|
|
3484
|
+
const willHaveVariableSet = input.payload.variableSetId !== void 0 ? input.payload.variableSetId !== null : Boolean(input.existing.variableSetId);
|
|
3485
|
+
if (willHaveVariableSet) {
|
|
3486
|
+
requirePermission(input.grant, "variable-sets:use");
|
|
2651
3487
|
}
|
|
2652
3488
|
update.agentConfig = await validateScheduledTaskAgentConfig({
|
|
2653
3489
|
settings: input.settings,
|
|
@@ -2663,7 +3499,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2663
3499
|
async function requireScheduledTaskForApi(db, workspaceId, taskId) {
|
|
2664
3500
|
const task = await getScheduledTask(db, workspaceId, taskId);
|
|
2665
3501
|
if (!task) {
|
|
2666
|
-
throw new
|
|
3502
|
+
throw new HTTPException10(404, { message: "scheduled task not found" });
|
|
2667
3503
|
}
|
|
2668
3504
|
return task;
|
|
2669
3505
|
}
|
|
@@ -2676,7 +3512,7 @@ async function restoreScheduledTask(db, task) {
|
|
|
2676
3512
|
overlapPolicy: task.overlapPolicy,
|
|
2677
3513
|
agentConfig: task.agentConfig,
|
|
2678
3514
|
reusableSessionId: task.reusableSessionId,
|
|
2679
|
-
|
|
3515
|
+
variableSetId: task.variableSetId,
|
|
2680
3516
|
metadata: task.metadata
|
|
2681
3517
|
});
|
|
2682
3518
|
}
|
|
@@ -2684,7 +3520,9 @@ async function syncCreatedScheduledTask(input) {
|
|
|
2684
3520
|
try {
|
|
2685
3521
|
await input.workflowClient.syncScheduledTask({ task: input.task });
|
|
2686
3522
|
} catch (error) {
|
|
2687
|
-
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
|
|
3523
|
+
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
|
|
3524
|
+
() => void 0
|
|
3525
|
+
);
|
|
2688
3526
|
throw error;
|
|
2689
3527
|
}
|
|
2690
3528
|
}
|
|
@@ -2715,17 +3553,27 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
|
|
|
2715
3553
|
}
|
|
2716
3554
|
async function validateScheduledTaskAgentConfig(input) {
|
|
2717
3555
|
assertConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
3556
|
+
await assertWorkspaceModelPolicyAllows(
|
|
3557
|
+
input.db,
|
|
3558
|
+
input.settings,
|
|
3559
|
+
input.workspaceId,
|
|
3560
|
+
input.payload.agentConfig.model
|
|
3561
|
+
);
|
|
2718
3562
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
2719
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3563
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3564
|
+
input.db,
|
|
3565
|
+
input.workspaceId,
|
|
3566
|
+
input.settings
|
|
3567
|
+
);
|
|
2720
3568
|
const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
|
|
2721
3569
|
const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
|
|
2722
3570
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
2723
3571
|
if (!prompt) {
|
|
2724
|
-
throw new
|
|
3572
|
+
throw new HTTPException10(422, { message: "scheduled task prompt is required" });
|
|
2725
3573
|
}
|
|
2726
3574
|
await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
|
|
2727
3575
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
2728
|
-
throw new
|
|
3576
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
2729
3577
|
}
|
|
2730
3578
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
2731
3579
|
return {
|
|
@@ -2740,19 +3588,19 @@ function validateScheduledTaskSchedule(schedule) {
|
|
|
2740
3588
|
return;
|
|
2741
3589
|
}
|
|
2742
3590
|
if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
|
|
2743
|
-
throw new
|
|
3591
|
+
throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
|
|
2744
3592
|
}
|
|
2745
3593
|
}
|
|
2746
3594
|
function trimmedScheduledTaskName(name) {
|
|
2747
3595
|
const trimmed = name.trim();
|
|
2748
3596
|
if (!trimmed) {
|
|
2749
|
-
throw new
|
|
3597
|
+
throw new HTTPException10(422, { message: "scheduled task name is required" });
|
|
2750
3598
|
}
|
|
2751
3599
|
return trimmed;
|
|
2752
3600
|
}
|
|
2753
3601
|
|
|
2754
3602
|
// src/domain/workspace-members.ts
|
|
2755
|
-
import { HTTPException as
|
|
3603
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
2756
3604
|
var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
|
|
2757
3605
|
function memberCanAdminister(member) {
|
|
2758
3606
|
return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
|
|
@@ -2762,110 +3610,540 @@ function isUserMember(member) {
|
|
|
2762
3610
|
}
|
|
2763
3611
|
function resolveMemberSubjectId(userId) {
|
|
2764
3612
|
if (!userId) {
|
|
2765
|
-
throw new
|
|
3613
|
+
throw new HTTPException11(404, { message: "user is not registered" });
|
|
2766
3614
|
}
|
|
2767
3615
|
return `user:${userId}`;
|
|
2768
3616
|
}
|
|
2769
3617
|
function assertWorkspaceMemberRemovable(input) {
|
|
2770
3618
|
const { members, subjectId, callerSubjectId } = input;
|
|
2771
3619
|
if (subjectId === callerSubjectId) {
|
|
2772
|
-
throw new
|
|
3620
|
+
throw new HTTPException11(409, { message: "you cannot remove your own membership" });
|
|
2773
3621
|
}
|
|
2774
3622
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
2775
3623
|
if (!target) {
|
|
2776
|
-
throw new
|
|
3624
|
+
throw new HTTPException11(404, { message: "member not found" });
|
|
2777
3625
|
}
|
|
2778
3626
|
if (memberCanAdminister(target)) {
|
|
2779
|
-
const remainingAdmins = members.filter(
|
|
3627
|
+
const remainingAdmins = members.filter(
|
|
3628
|
+
(member) => member.subjectId !== subjectId && memberCanAdminister(member)
|
|
3629
|
+
);
|
|
2780
3630
|
if (remainingAdmins.length === 0) {
|
|
2781
|
-
throw new
|
|
3631
|
+
throw new HTTPException11(409, {
|
|
3632
|
+
message: "cannot remove the last member who can manage this workspace"
|
|
3633
|
+
});
|
|
2782
3634
|
}
|
|
2783
3635
|
}
|
|
2784
3636
|
}
|
|
2785
3637
|
function assertWorkspaceDeletable(input) {
|
|
2786
3638
|
if (input.workspaceCountForAccount <= 1) {
|
|
2787
|
-
throw new
|
|
3639
|
+
throw new HTTPException11(409, { message: "cannot delete the account's only workspace" });
|
|
2788
3640
|
}
|
|
2789
3641
|
if (input.activeSessionCount > 0) {
|
|
2790
|
-
throw new
|
|
3642
|
+
throw new HTTPException11(409, {
|
|
2791
3643
|
message: "stop the workspace's running sessions before deleting it"
|
|
2792
3644
|
});
|
|
2793
3645
|
}
|
|
2794
3646
|
}
|
|
3647
|
+
|
|
3648
|
+
// src/application/session-commands.ts
|
|
3649
|
+
import { reasoningEffortForMetadata as reasoningEffortForMetadata2 } from "@opengeni/contracts";
|
|
3650
|
+
import {
|
|
3651
|
+
deleteSessionQueueItemInTransaction,
|
|
3652
|
+
editQueuedTurnInTransaction,
|
|
3653
|
+
getComposerDraftInTransaction,
|
|
3654
|
+
getSession as getSession2,
|
|
3655
|
+
getSessionEvent as getSessionEvent2,
|
|
3656
|
+
getWorkspaceControlEvent as getWorkspaceControlEvent2,
|
|
3657
|
+
getSessionQueueSnapshot,
|
|
3658
|
+
moveQueuedTurnInTransaction,
|
|
3659
|
+
mutateSessionControlInTransaction,
|
|
3660
|
+
mutateWorkspaceControlInTransaction,
|
|
3661
|
+
saveComposerDraftInTransaction,
|
|
3662
|
+
sendAgentMessageInTransaction,
|
|
3663
|
+
serializeEffectiveSessionControl,
|
|
3664
|
+
steerAgentSessionInTransaction,
|
|
3665
|
+
steerQueuedTurnInTransaction,
|
|
3666
|
+
withWorkspaceRls,
|
|
3667
|
+
withWorkspaceSubjectRls as withWorkspaceSubjectRls2
|
|
3668
|
+
} from "@opengeni/db";
|
|
3669
|
+
import {
|
|
3670
|
+
publishDurableSessionEvents as publishDurableSessionEvents2,
|
|
3671
|
+
publishDurableWorkspaceControlEvent as publishDurableWorkspaceControlEvent2
|
|
3672
|
+
} from "@opengeni/events";
|
|
3673
|
+
function agentActor(context) {
|
|
3674
|
+
return {
|
|
3675
|
+
type: "agent_attempt",
|
|
3676
|
+
sessionId: context.callerSessionId,
|
|
3677
|
+
turnId: context.callerTurnId,
|
|
3678
|
+
attemptId: context.callerAttemptId,
|
|
3679
|
+
executionGeneration: context.callerExecutionGeneration
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
async function publishAndWakeAgentCommand(deps, input) {
|
|
3683
|
+
await publishSessionEventIds(deps, input.workspaceId, input.sessionId, input.eventIds);
|
|
3684
|
+
if (!input.shouldSignal || input.wakeRevision === null) return;
|
|
3685
|
+
try {
|
|
3686
|
+
await deps.workflowClient.wakeSessionWorkflow({
|
|
3687
|
+
accountId: input.accountId,
|
|
3688
|
+
workspaceId: input.workspaceId,
|
|
3689
|
+
sessionId: input.sessionId,
|
|
3690
|
+
workflowId: input.workflowId,
|
|
3691
|
+
wakeRevision: input.wakeRevision,
|
|
3692
|
+
...input.interruptionCount > 0 ? { interruptionRequested: true } : {}
|
|
3693
|
+
});
|
|
3694
|
+
} catch (error) {
|
|
3695
|
+
console.warn(
|
|
3696
|
+
`[session-commands] immediate Agent command wake failed for ${input.workspaceId}/${input.sessionId}; durable outbox will retry`,
|
|
3697
|
+
error
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
async function requestControlWakeDispatch(deps, wakeCount) {
|
|
3702
|
+
if (wakeCount === 0) return;
|
|
3703
|
+
try {
|
|
3704
|
+
await deps.workflowClient.requestSessionWorkflowWakeDispatch();
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
console.warn(
|
|
3707
|
+
`[session-commands] immediate control wake dispatch failed for ${wakeCount} committed revisions; durable outbox will retry`,
|
|
3708
|
+
error
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
async function publishSessionEventIds(deps, workspaceId, sessionId, eventIds) {
|
|
3713
|
+
if (eventIds.length === 0) return;
|
|
3714
|
+
const events = await Promise.all(
|
|
3715
|
+
eventIds.map((eventId) => getSessionEvent2(deps.db, workspaceId, eventId))
|
|
3716
|
+
);
|
|
3717
|
+
await publishDurableSessionEvents2(
|
|
3718
|
+
deps.bus,
|
|
3719
|
+
workspaceId,
|
|
3720
|
+
sessionId,
|
|
3721
|
+
events.filter((event) => event !== null)
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3724
|
+
async function publishWorkspaceControlEvent(deps, workspaceId, eventId) {
|
|
3725
|
+
if (!eventId) return;
|
|
3726
|
+
const event = await getWorkspaceControlEvent2(deps.db, workspaceId, eventId);
|
|
3727
|
+
if (!event) {
|
|
3728
|
+
throw new Error(`Committed workspace control event disappeared: ${eventId}`);
|
|
3729
|
+
}
|
|
3730
|
+
await publishDurableWorkspaceControlEvent2(deps.bus, workspaceId, event);
|
|
3731
|
+
}
|
|
3732
|
+
async function sendAgentSessionMessage(deps, context, input) {
|
|
3733
|
+
const result = await withWorkspaceRls(
|
|
3734
|
+
deps.db,
|
|
3735
|
+
context.workspaceId,
|
|
3736
|
+
(scoped) => scoped.transaction(
|
|
3737
|
+
(tx) => sendAgentMessageInTransaction(tx, {
|
|
3738
|
+
accountId: context.accountId,
|
|
3739
|
+
workspaceId: context.workspaceId,
|
|
3740
|
+
targetSessionId: input.targetSessionId,
|
|
3741
|
+
actor: agentActor(context),
|
|
3742
|
+
operationKey: input.idempotencyKey,
|
|
3743
|
+
text: input.text
|
|
3744
|
+
})
|
|
3745
|
+
)
|
|
3746
|
+
);
|
|
3747
|
+
await publishAndWakeAgentCommand(deps, {
|
|
3748
|
+
accountId: context.accountId,
|
|
3749
|
+
workspaceId: context.workspaceId,
|
|
3750
|
+
sessionId: input.targetSessionId,
|
|
3751
|
+
eventIds: result.eventIds,
|
|
3752
|
+
workflowId: result.workflowId,
|
|
3753
|
+
wakeRevision: result.wakeRevision,
|
|
3754
|
+
shouldSignal: result.shouldSignal,
|
|
3755
|
+
interruptionCount: 0
|
|
3756
|
+
});
|
|
3757
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3758
|
+
return result;
|
|
3759
|
+
}
|
|
3760
|
+
async function steerAgentSession(deps, context, input) {
|
|
3761
|
+
const result = await withWorkspaceRls(
|
|
3762
|
+
deps.db,
|
|
3763
|
+
context.workspaceId,
|
|
3764
|
+
(scoped) => scoped.transaction(
|
|
3765
|
+
(tx) => steerAgentSessionInTransaction(tx, {
|
|
3766
|
+
accountId: context.accountId,
|
|
3767
|
+
workspaceId: context.workspaceId,
|
|
3768
|
+
targetSessionId: input.targetSessionId,
|
|
3769
|
+
actor: agentActor(context),
|
|
3770
|
+
operationKey: input.idempotencyKey,
|
|
3771
|
+
instruction: input.instruction
|
|
3772
|
+
})
|
|
3773
|
+
)
|
|
3774
|
+
);
|
|
3775
|
+
await publishAndWakeAgentCommand(deps, {
|
|
3776
|
+
accountId: context.accountId,
|
|
3777
|
+
workspaceId: context.workspaceId,
|
|
3778
|
+
sessionId: input.targetSessionId,
|
|
3779
|
+
eventIds: result.eventIds,
|
|
3780
|
+
workflowId: result.workflowId,
|
|
3781
|
+
wakeRevision: result.wakeRevision,
|
|
3782
|
+
shouldSignal: result.shouldSignal,
|
|
3783
|
+
interruptionCount: result.interruptionCount
|
|
3784
|
+
});
|
|
3785
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3786
|
+
return result;
|
|
3787
|
+
}
|
|
3788
|
+
async function controlAgentSessionWorkstream(deps, context, input) {
|
|
3789
|
+
const result = await withWorkspaceRls(
|
|
3790
|
+
deps.db,
|
|
3791
|
+
context.workspaceId,
|
|
3792
|
+
(scoped) => scoped.transaction(
|
|
3793
|
+
(tx) => mutateSessionControlInTransaction(tx, {
|
|
3794
|
+
accountId: context.accountId,
|
|
3795
|
+
workspaceId: context.workspaceId,
|
|
3796
|
+
sessionId: input.targetSessionId,
|
|
3797
|
+
actor: agentActor(context),
|
|
3798
|
+
operationKey: input.idempotencyKey,
|
|
3799
|
+
action: input.action,
|
|
3800
|
+
reason: input.reason ?? null
|
|
3801
|
+
})
|
|
3802
|
+
)
|
|
3803
|
+
);
|
|
3804
|
+
await publishSessionEventIds(deps, context.workspaceId, input.targetSessionId, [
|
|
3805
|
+
result.sessionControlEventId
|
|
3806
|
+
]);
|
|
3807
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3808
|
+
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
3809
|
+
return result;
|
|
3810
|
+
}
|
|
3811
|
+
function receipt(row) {
|
|
3812
|
+
return {
|
|
3813
|
+
id: row.id,
|
|
3814
|
+
action: row.action,
|
|
3815
|
+
operationKey: row.operationKey,
|
|
3816
|
+
targetSessionId: row.targetSessionId,
|
|
3817
|
+
targetTurnId: row.targetTurnId,
|
|
3818
|
+
appliedControlRevision: row.appliedControlRevision,
|
|
3819
|
+
appliedQueueVersion: row.appliedQueueVersion,
|
|
3820
|
+
appliedTurnVersion: row.appliedTurnVersion,
|
|
3821
|
+
appliedDraftRevision: row.appliedDraftRevision,
|
|
3822
|
+
createdAt: row.createdAt.toISOString()
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
function composerDraft(row) {
|
|
3826
|
+
if (!row) return null;
|
|
3827
|
+
return {
|
|
3828
|
+
revision: row.revision,
|
|
3829
|
+
text: row.text,
|
|
3830
|
+
resources: row.resources,
|
|
3831
|
+
tools: row.tools,
|
|
3832
|
+
model: row.model,
|
|
3833
|
+
reasoningEffort: row.reasoningEffort,
|
|
3834
|
+
sourceTurnId: row.sourceTurnId,
|
|
3835
|
+
sourceTurnVersion: row.sourceTurnVersion,
|
|
3836
|
+
updatedAt: row.updatedAt.toISOString()
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
async function authoritativeQueue(db, workspaceId, sessionId) {
|
|
3840
|
+
const snapshot = await getSessionQueueSnapshot(db, workspaceId, sessionId);
|
|
3841
|
+
if (!snapshot) throw new Error(`Session not found: ${sessionId}`);
|
|
3842
|
+
return snapshot;
|
|
3843
|
+
}
|
|
3844
|
+
async function moveHumanQueuePrompt(deps, context, turnId, input) {
|
|
3845
|
+
const result = await withWorkspaceRls(
|
|
3846
|
+
deps.db,
|
|
3847
|
+
context.workspaceId,
|
|
3848
|
+
(scoped) => scoped.transaction(
|
|
3849
|
+
(tx) => moveQueuedTurnInTransaction(tx, {
|
|
3850
|
+
...context,
|
|
3851
|
+
turnId,
|
|
3852
|
+
beforeTurnId: input.beforeTurnId,
|
|
3853
|
+
expectedQueueVersion: input.expectedQueueVersion,
|
|
3854
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3855
|
+
operationKey: input.clientEventId
|
|
3856
|
+
})
|
|
3857
|
+
)
|
|
3858
|
+
);
|
|
3859
|
+
const response = {
|
|
3860
|
+
receipt: receipt(result.receipt),
|
|
3861
|
+
snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
|
|
3862
|
+
};
|
|
3863
|
+
await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
|
|
3864
|
+
return response;
|
|
3865
|
+
}
|
|
3866
|
+
async function deleteHumanQueuePrompt(deps, context, turnId, input) {
|
|
3867
|
+
const result = await withWorkspaceRls(
|
|
3868
|
+
deps.db,
|
|
3869
|
+
context.workspaceId,
|
|
3870
|
+
(scoped) => scoped.transaction(
|
|
3871
|
+
(tx) => deleteSessionQueueItemInTransaction(tx, {
|
|
3872
|
+
...context,
|
|
3873
|
+
turnId,
|
|
3874
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
3875
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3876
|
+
operationKey: input.clientEventId,
|
|
3877
|
+
reason: input.reason ?? null
|
|
3878
|
+
})
|
|
3879
|
+
)
|
|
3880
|
+
);
|
|
3881
|
+
const response = {
|
|
3882
|
+
receipt: receipt(result.receipt),
|
|
3883
|
+
snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
|
|
3884
|
+
};
|
|
3885
|
+
await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
|
|
3886
|
+
return response;
|
|
3887
|
+
}
|
|
3888
|
+
async function editHumanQueuePrompt(deps, context, turnId, input) {
|
|
3889
|
+
const result = await withWorkspaceSubjectRls2(
|
|
3890
|
+
deps.db,
|
|
3891
|
+
context.workspaceId,
|
|
3892
|
+
context.subjectId,
|
|
3893
|
+
(scoped) => scoped.transaction(
|
|
3894
|
+
(tx) => editQueuedTurnInTransaction(tx, {
|
|
3895
|
+
...context,
|
|
3896
|
+
turnId,
|
|
3897
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
3898
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
3899
|
+
replaceDraft: input.replaceDraft,
|
|
3900
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3901
|
+
operationKey: input.clientEventId
|
|
3902
|
+
})
|
|
3903
|
+
)
|
|
3904
|
+
);
|
|
3905
|
+
const response = {
|
|
3906
|
+
receipt: receipt(result.receipt),
|
|
3907
|
+
snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId),
|
|
3908
|
+
draft: composerDraft(result.draft)
|
|
3909
|
+
};
|
|
3910
|
+
await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
|
|
3911
|
+
return response;
|
|
3912
|
+
}
|
|
3913
|
+
async function steerHumanQueuePrompt(deps, context, turnId, input) {
|
|
3914
|
+
const result = await withWorkspaceRls(
|
|
3915
|
+
deps.db,
|
|
3916
|
+
context.workspaceId,
|
|
3917
|
+
(scoped) => scoped.transaction(
|
|
3918
|
+
(tx) => steerQueuedTurnInTransaction(tx, {
|
|
3919
|
+
...context,
|
|
3920
|
+
turnId,
|
|
3921
|
+
expectedTurnVersion: input.expectedTurnVersion,
|
|
3922
|
+
controlEtag: input.controlEtag ?? null,
|
|
3923
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3924
|
+
operationKey: input.clientEventId
|
|
3925
|
+
})
|
|
3926
|
+
)
|
|
3927
|
+
);
|
|
3928
|
+
const response = {
|
|
3929
|
+
receipt: receipt(result.receipt),
|
|
3930
|
+
snapshot: await authoritativeQueue(deps.db, context.workspaceId, context.sessionId)
|
|
3931
|
+
};
|
|
3932
|
+
await publishSessionEventIds(deps, context.workspaceId, context.sessionId, result.eventIds);
|
|
3933
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3934
|
+
return response;
|
|
3935
|
+
}
|
|
3936
|
+
async function controlHumanSessionWorkstream(deps, context, input) {
|
|
3937
|
+
const result = await withWorkspaceRls(
|
|
3938
|
+
deps.db,
|
|
3939
|
+
context.workspaceId,
|
|
3940
|
+
(scoped) => scoped.transaction(
|
|
3941
|
+
(tx) => mutateSessionControlInTransaction(tx, {
|
|
3942
|
+
accountId: context.accountId,
|
|
3943
|
+
workspaceId: context.workspaceId,
|
|
3944
|
+
sessionId: context.sessionId,
|
|
3945
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3946
|
+
operationKey: input.clientEventId,
|
|
3947
|
+
action: input.action,
|
|
3948
|
+
reason: input.reason ?? null,
|
|
3949
|
+
expectedControlEtag: input.expectedControlEtag ?? null
|
|
3950
|
+
})
|
|
3951
|
+
)
|
|
3952
|
+
);
|
|
3953
|
+
const response = {
|
|
3954
|
+
receipt: receipt(result.receipt),
|
|
3955
|
+
effectiveControl: serializeEffectiveSessionControl(result.control),
|
|
3956
|
+
interruptionCount: result.interruptionCount,
|
|
3957
|
+
wakeCount: result.wakeCount
|
|
3958
|
+
};
|
|
3959
|
+
await publishSessionEventIds(deps, context.workspaceId, context.sessionId, [
|
|
3960
|
+
result.sessionControlEventId
|
|
3961
|
+
]);
|
|
3962
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3963
|
+
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
3964
|
+
return response;
|
|
3965
|
+
}
|
|
3966
|
+
async function controlHumanWorkspace(deps, context, input) {
|
|
3967
|
+
const result = await withWorkspaceRls(
|
|
3968
|
+
deps.db,
|
|
3969
|
+
context.workspaceId,
|
|
3970
|
+
(scoped) => scoped.transaction(
|
|
3971
|
+
(tx) => mutateWorkspaceControlInTransaction(tx, {
|
|
3972
|
+
accountId: context.accountId,
|
|
3973
|
+
workspaceId: context.workspaceId,
|
|
3974
|
+
actor: { type: "human", subjectId: context.subjectId },
|
|
3975
|
+
operationKey: input.clientEventId,
|
|
3976
|
+
action: input.action,
|
|
3977
|
+
reason: input.reason ?? null,
|
|
3978
|
+
expectedRevision: input.expectedRevision ?? null
|
|
3979
|
+
})
|
|
3980
|
+
)
|
|
3981
|
+
);
|
|
3982
|
+
const response = {
|
|
3983
|
+
receipt: receipt(result.receipt),
|
|
3984
|
+
state: result.workspaceState,
|
|
3985
|
+
revision: result.revision,
|
|
3986
|
+
interruptionCount: result.interruptionCount,
|
|
3987
|
+
wakeCount: result.wakeCount
|
|
3988
|
+
};
|
|
3989
|
+
await publishWorkspaceControlEvent(deps, context.workspaceId, result.workspaceControlEventId);
|
|
3990
|
+
await requestControlWakeDispatch(deps, result.wakeCount);
|
|
3991
|
+
return response;
|
|
3992
|
+
}
|
|
3993
|
+
async function getHumanComposerDraft(db, context) {
|
|
3994
|
+
const row = await withWorkspaceSubjectRls2(
|
|
3995
|
+
db,
|
|
3996
|
+
context.workspaceId,
|
|
3997
|
+
context.subjectId,
|
|
3998
|
+
(scoped) => getComposerDraftInTransaction(scoped, {
|
|
3999
|
+
workspaceId: context.workspaceId,
|
|
4000
|
+
sessionId: context.sessionId,
|
|
4001
|
+
subjectId: context.subjectId
|
|
4002
|
+
})
|
|
4003
|
+
);
|
|
4004
|
+
const mapped = composerDraft(row);
|
|
4005
|
+
if (mapped) return mapped;
|
|
4006
|
+
const session = await getSession2(db, context.workspaceId, context.sessionId);
|
|
4007
|
+
if (!session) throw new Error(`Session not found: ${context.sessionId}`);
|
|
4008
|
+
return {
|
|
4009
|
+
revision: 0,
|
|
4010
|
+
text: "",
|
|
4011
|
+
resources: [],
|
|
4012
|
+
tools: [],
|
|
4013
|
+
model: session.model,
|
|
4014
|
+
reasoningEffort: reasoningEffortForMetadata2(session.metadata, "medium"),
|
|
4015
|
+
sourceTurnId: null,
|
|
4016
|
+
sourceTurnVersion: null,
|
|
4017
|
+
updatedAt: null
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
async function saveHumanComposerDraft(db, context, input) {
|
|
4021
|
+
const row = await withWorkspaceSubjectRls2(
|
|
4022
|
+
db,
|
|
4023
|
+
context.workspaceId,
|
|
4024
|
+
context.subjectId,
|
|
4025
|
+
(scoped) => scoped.transaction(
|
|
4026
|
+
(tx) => saveComposerDraftInTransaction(tx, {
|
|
4027
|
+
...context,
|
|
4028
|
+
...input,
|
|
4029
|
+
subjectId: context.subjectId
|
|
4030
|
+
})
|
|
4031
|
+
)
|
|
4032
|
+
);
|
|
4033
|
+
return composerDraft(row);
|
|
4034
|
+
}
|
|
2795
4035
|
export {
|
|
2796
4036
|
MARKETING_SOCIAL_PACK_ID,
|
|
4037
|
+
MAX_CHECKS_PER_RIG,
|
|
4038
|
+
MAX_CREDENTIAL_HOOKS_PER_RIG,
|
|
4039
|
+
MAX_DEFAULT_VARIABLE_SETS_PER_RIG,
|
|
2797
4040
|
MAX_ENVIRONMENTS_PER_WORKSPACE,
|
|
4041
|
+
MAX_RIGS_PER_WORKSPACE,
|
|
2798
4042
|
MAX_VARIABLES_PER_ENVIRONMENT,
|
|
4043
|
+
SESSION_WORKFLOW_WAKE_DISPATCHER_PERIOD_MS,
|
|
4044
|
+
SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID,
|
|
4045
|
+
SESSION_WORKFLOW_WAKE_DISPATCHER_WORKFLOW_TYPE,
|
|
2799
4046
|
acceptSessionUserMessage,
|
|
4047
|
+
activateRigVersionForApi,
|
|
4048
|
+
appendRigSetupCommand,
|
|
2800
4049
|
applyCapabilityEnablement,
|
|
2801
4050
|
assertAllowedEnvironmentVariableName,
|
|
4051
|
+
assertAllowedVariableSetVariableName,
|
|
2802
4052
|
assertConfiguredModel,
|
|
2803
4053
|
assertPackSandboxImageCompatible,
|
|
2804
4054
|
assertWorkspaceDeletable,
|
|
2805
4055
|
assertWorkspaceMemberRemovable,
|
|
4056
|
+
assertWorkspaceModelPolicyAllows,
|
|
2806
4057
|
buildCapabilityCatalog,
|
|
2807
4058
|
buildFleetContextForSession,
|
|
2808
4059
|
buildMarketingDailyAnalysisAgentConfig,
|
|
2809
4060
|
checkLimit,
|
|
4061
|
+
classifyRigVerificationOutcome,
|
|
4062
|
+
controlAgentSessionWorkstream,
|
|
4063
|
+
controlHumanSessionWorkstream,
|
|
4064
|
+
controlHumanWorkspace,
|
|
2810
4065
|
createAndStartSession,
|
|
2811
4066
|
createCatalogItem,
|
|
4067
|
+
createRigForApi,
|
|
4068
|
+
createRigVersionForApi,
|
|
2812
4069
|
createSessionForRequest,
|
|
2813
4070
|
createValidatedScheduledTask,
|
|
4071
|
+
deleteHumanQueuePrompt,
|
|
4072
|
+
deleteRigForApi,
|
|
2814
4073
|
disableCapability,
|
|
2815
4074
|
discoverMcpRegistryCapabilities,
|
|
4075
|
+
editHumanQueuePrompt,
|
|
2816
4076
|
enableCapability,
|
|
2817
4077
|
enabledCapabilityMcpToolRefs,
|
|
2818
4078
|
getCapabilityPack,
|
|
4079
|
+
getHumanComposerDraft,
|
|
2819
4080
|
hasPermission,
|
|
2820
4081
|
isBuiltInCapabilityPack,
|
|
2821
4082
|
isUserMember,
|
|
2822
4083
|
listCapabilityPacks,
|
|
2823
4084
|
listFleet,
|
|
4085
|
+
listRigChangesForApi,
|
|
4086
|
+
listRigVersionsForApi,
|
|
2824
4087
|
listWorkspaceCapabilityPacks,
|
|
2825
4088
|
manualScheduledTaskTriggerUsageKey,
|
|
2826
4089
|
manualScheduledTaskTriggerWorkflowId,
|
|
2827
4090
|
memberCanAdminister,
|
|
2828
4091
|
mergeResourceRefs,
|
|
2829
4092
|
mergeToolRefs,
|
|
4093
|
+
moveHumanQueuePrompt,
|
|
2830
4094
|
normalizeResources,
|
|
2831
4095
|
officialMcpRegistryUrl,
|
|
2832
4096
|
postUserMessageTurn,
|
|
4097
|
+
promoteSetupAppendChange,
|
|
4098
|
+
promoteVerifiedDefinitionEditChangeForApi,
|
|
4099
|
+
proposeRigChangeForApi,
|
|
2833
4100
|
provisionSandbox,
|
|
4101
|
+
readSessionLineage,
|
|
2834
4102
|
reasoningEffortForSession,
|
|
2835
|
-
|
|
4103
|
+
recordRigAuditEvent,
|
|
4104
|
+
recordVariableSetAuditEvent,
|
|
2836
4105
|
recordWorkspaceUsage,
|
|
2837
4106
|
relayConfigFromSettings,
|
|
2838
4107
|
relayDialBaseFromSettings,
|
|
2839
4108
|
requireAccessContext,
|
|
2840
4109
|
requireAccessGrant,
|
|
2841
4110
|
requireEnvironmentEncryption,
|
|
2842
|
-
requireEnvironmentForApi,
|
|
2843
4111
|
requireLimit,
|
|
2844
4112
|
requirePermission,
|
|
2845
4113
|
requireQueuedTurnForApi,
|
|
4114
|
+
requireRigChangeForApi,
|
|
4115
|
+
requireRigForApi,
|
|
2846
4116
|
requireScheduledTaskForApi,
|
|
4117
|
+
requireVariableSetEncryption,
|
|
4118
|
+
requireVariableSetForApi,
|
|
2847
4119
|
resolveCapabilityPack,
|
|
2848
4120
|
resolveMemberSubjectId,
|
|
2849
4121
|
restoreScheduledTask,
|
|
4122
|
+
rigActorForGrant,
|
|
2850
4123
|
routingEnabled,
|
|
2851
4124
|
runOnSandbox,
|
|
4125
|
+
saveHumanComposerDraft,
|
|
2852
4126
|
scheduledTaskTemporalScheduleId,
|
|
2853
4127
|
scheduledTaskToolsProvided,
|
|
2854
4128
|
scheduledTaskTriggerToken,
|
|
4129
|
+
sendAgentSessionMessage,
|
|
2855
4130
|
settingsWithEnabledCapabilityMcpServers,
|
|
2856
4131
|
settingsWithMcpCapabilityServers,
|
|
2857
4132
|
settingsWithSessionMcpServerMetadata,
|
|
2858
4133
|
stableJson,
|
|
4134
|
+
steerAgentSession,
|
|
4135
|
+
steerHumanQueuePrompt,
|
|
2859
4136
|
swapActiveSandbox,
|
|
2860
4137
|
syncCreatedScheduledTask,
|
|
2861
4138
|
syncUpdatedScheduledTask,
|
|
4139
|
+
updateRigForApi,
|
|
2862
4140
|
updateSessionTitle,
|
|
2863
|
-
validateEnvironmentAttachment,
|
|
2864
4141
|
validateFileResources,
|
|
2865
4142
|
validateGitHubRepositorySelection,
|
|
2866
4143
|
validateGitHubRepositorySelectionShape,
|
|
2867
4144
|
validateMcpCapabilityConnection,
|
|
2868
4145
|
validateToolRefs,
|
|
4146
|
+
validateVariableSetAttachment,
|
|
2869
4147
|
validatedScheduledTaskUpdate,
|
|
2870
4148
|
withDefaultEnabledCapabilityMcpTools,
|
|
2871
4149
|
workflowIdForSession,
|