@opengeni/core 0.4.5 → 0.4.7
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 +179 -30
- package/dist/index.js +1324 -477
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
- package/src/access/index.ts +132 -57
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +76 -14
- package/src/domain/capabilities.ts +388 -182
- 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 +514 -258
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +1 -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/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
NatsControlRpc as NatsControlRpc2,
|
|
12
12
|
selfhostedLiveness,
|
|
13
|
-
SelfhostedSession
|
|
13
|
+
SelfhostedSession,
|
|
14
|
+
swapTargetEstablishability
|
|
14
15
|
} from "@opengeni/runtime/sandbox";
|
|
15
16
|
import { HTTPException } from "hono/http-exception";
|
|
16
17
|
|
|
@@ -63,7 +64,12 @@ function wrapChannelABoxWithRouting(services, ids, established) {
|
|
|
63
64
|
defaultKind: established.backendId,
|
|
64
65
|
getSandbox: async (sandboxId) => {
|
|
65
66
|
const sandbox = await getSandbox(db, ids.workspaceId, sandboxId);
|
|
66
|
-
return sandbox ? {
|
|
67
|
+
return sandbox ? {
|
|
68
|
+
id: sandbox.id,
|
|
69
|
+
kind: sandbox.kind,
|
|
70
|
+
name: sandbox.name,
|
|
71
|
+
enrollmentId: sandbox.enrollmentId
|
|
72
|
+
} : null;
|
|
67
73
|
},
|
|
68
74
|
controlRpcFactory: controlRpcFactory(bus),
|
|
69
75
|
relay: relayConfigFromSettings(settings)
|
|
@@ -126,7 +132,9 @@ async function probeEnrollment(services, workspaceId, enrollment) {
|
|
|
126
132
|
exposure: enrollment.exposure,
|
|
127
133
|
allowScreenControl: enrollment.allowScreenControl,
|
|
128
134
|
hasDisplay: enrollment.hasDisplay,
|
|
129
|
-
lastSeenAt: enrollment.lastSeenAt
|
|
135
|
+
lastSeenAt: enrollment.lastSeenAt,
|
|
136
|
+
wentOfflineAt: enrollment.wentOfflineAt,
|
|
137
|
+
wentOfflineReason: enrollment.wentOfflineReason
|
|
130
138
|
},
|
|
131
139
|
probeResponded
|
|
132
140
|
});
|
|
@@ -171,7 +179,11 @@ async function listFleet(services, ctx) {
|
|
|
171
179
|
lastSeenAt: enrollment?.lastSeenAt ?? null
|
|
172
180
|
});
|
|
173
181
|
}
|
|
174
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
activeSandboxId: pointer.activeSandboxId,
|
|
184
|
+
activeEpoch: pointer.activeEpoch,
|
|
185
|
+
sandboxes: entries
|
|
186
|
+
};
|
|
175
187
|
}
|
|
176
188
|
async function resolveTarget(services, ctx, target) {
|
|
177
189
|
if (target === ctx.sessionGroupId || target === "session" || target === "default") {
|
|
@@ -179,19 +191,42 @@ async function resolveTarget(services, ctx, target) {
|
|
|
179
191
|
}
|
|
180
192
|
const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
|
|
181
193
|
if (!sandbox) {
|
|
182
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
reason: `sandbox ${target} not found in this workspace`,
|
|
197
|
+
code: "stale_pointer"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const establishable = swapTargetEstablishability({
|
|
201
|
+
kind: sandbox.kind,
|
|
202
|
+
isSessionGroup: false
|
|
203
|
+
});
|
|
204
|
+
if (!establishable.ok) {
|
|
205
|
+
return { ok: false, reason: establishable.reason, code: establishable.code };
|
|
183
206
|
}
|
|
184
207
|
if (sandbox.kind === "selfhosted") {
|
|
185
208
|
if (!sandbox.enrollmentId) {
|
|
186
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
reason: `selfhosted sandbox ${target} has no enrollment`,
|
|
212
|
+
code: "offline_enrollment"
|
|
213
|
+
};
|
|
187
214
|
}
|
|
188
215
|
const enrollment = await getEnrollment(services.db, ctx.workspaceId, sandbox.enrollmentId);
|
|
189
216
|
if (!enrollment) {
|
|
190
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
reason: `enrollment for sandbox ${target} not found`,
|
|
220
|
+
code: "offline_enrollment"
|
|
221
|
+
};
|
|
191
222
|
}
|
|
192
223
|
const probe = await probeEnrollment(services, ctx.workspaceId, enrollment);
|
|
193
224
|
if (probe.liveness !== "online") {
|
|
194
|
-
return {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: `sandbox ${target} is ${probe.liveness}; cannot attach to a non-online machine`,
|
|
228
|
+
code: "offline_enrollment"
|
|
229
|
+
};
|
|
195
230
|
}
|
|
196
231
|
}
|
|
197
232
|
return { ok: true, targetSandboxId: sandbox.id };
|
|
@@ -203,7 +238,13 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
203
238
|
activeSandboxId: null,
|
|
204
239
|
activeEpoch: 0
|
|
205
240
|
};
|
|
206
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
swapped: false,
|
|
243
|
+
activeSandboxId: pointer2.activeSandboxId,
|
|
244
|
+
activeEpoch: pointer2.activeEpoch,
|
|
245
|
+
reason: resolved.reason,
|
|
246
|
+
code: resolved.code
|
|
247
|
+
};
|
|
207
248
|
}
|
|
208
249
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
209
250
|
const pointer2 = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
@@ -211,7 +252,11 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
211
252
|
activeEpoch: 0
|
|
212
253
|
};
|
|
213
254
|
if (pointer2.activeSandboxId === resolved.targetSandboxId) {
|
|
214
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
swapped: true,
|
|
257
|
+
activeSandboxId: pointer2.activeSandboxId,
|
|
258
|
+
activeEpoch: pointer2.activeEpoch
|
|
259
|
+
};
|
|
215
260
|
}
|
|
216
261
|
const result = await setActiveSandbox(services.db, {
|
|
217
262
|
accountId: ctx.accountId,
|
|
@@ -222,7 +267,11 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
222
267
|
...workingDir !== void 0 ? { workingDir } : {}
|
|
223
268
|
});
|
|
224
269
|
if (result.swapped && result.pointer) {
|
|
225
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
swapped: true,
|
|
272
|
+
activeSandboxId: result.pointer.activeSandboxId,
|
|
273
|
+
activeEpoch: result.pointer.activeEpoch
|
|
274
|
+
};
|
|
226
275
|
}
|
|
227
276
|
}
|
|
228
277
|
const pointer = await readActiveSandbox2(services.db, ctx.workspaceId, ctx.sessionId) ?? {
|
|
@@ -233,13 +282,19 @@ async function swapActiveSandbox(services, ctx, target, workingDir) {
|
|
|
233
282
|
swapped: false,
|
|
234
283
|
activeSandboxId: pointer.activeSandboxId,
|
|
235
284
|
activeEpoch: pointer.activeEpoch,
|
|
236
|
-
reason: "a concurrent swap won the epoch fence; re-read and retry"
|
|
285
|
+
reason: "a concurrent swap won the epoch fence; re-read and retry",
|
|
286
|
+
code: "concurrent_swap"
|
|
237
287
|
};
|
|
238
288
|
}
|
|
239
289
|
async function runOnSandbox(services, ctx, target, op) {
|
|
240
290
|
const sandbox = await getSandbox2(services.db, ctx.workspaceId, target);
|
|
241
291
|
if (!sandbox) {
|
|
242
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
target,
|
|
294
|
+
kind: op.kind,
|
|
295
|
+
ok: false,
|
|
296
|
+
reason: `sandbox ${target} not found in this workspace`
|
|
297
|
+
};
|
|
243
298
|
}
|
|
244
299
|
if (sandbox.kind !== "selfhosted" || !sandbox.enrollmentId) {
|
|
245
300
|
return {
|
|
@@ -261,8 +316,18 @@ async function runOnSandbox(services, ctx, target, op) {
|
|
|
261
316
|
});
|
|
262
317
|
try {
|
|
263
318
|
if (op.kind === "exec") {
|
|
264
|
-
const res = await session.exec({
|
|
265
|
-
|
|
319
|
+
const res = await session.exec({
|
|
320
|
+
cmd: op.cmd,
|
|
321
|
+
...op.workdir ? { workdir: op.workdir } : {}
|
|
322
|
+
});
|
|
323
|
+
return {
|
|
324
|
+
target,
|
|
325
|
+
kind: "exec",
|
|
326
|
+
ok: true,
|
|
327
|
+
stdout: res.stdout,
|
|
328
|
+
stderr: res.stderr,
|
|
329
|
+
exitCode: res.exitCode
|
|
330
|
+
};
|
|
266
331
|
}
|
|
267
332
|
if (op.kind === "read") {
|
|
268
333
|
const bytes = await session.readFile({ path: op.path });
|
|
@@ -301,12 +366,14 @@ async function provisionSandbox(services, ctx, input) {
|
|
|
301
366
|
return {
|
|
302
367
|
kind: "modal",
|
|
303
368
|
sandbox,
|
|
304
|
-
note: "A named Modal sandbox record was created
|
|
369
|
+
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
370
|
};
|
|
306
371
|
}
|
|
307
372
|
|
|
308
373
|
// src/access/index.ts
|
|
309
|
-
import {
|
|
374
|
+
import {
|
|
375
|
+
verifyDelegatedAccessToken
|
|
376
|
+
} from "@opengeni/contracts";
|
|
310
377
|
import {
|
|
311
378
|
bootstrapWorkspace,
|
|
312
379
|
ensureManagedAccessForUser,
|
|
@@ -340,11 +407,25 @@ async function requireAccessGrant(c, deps, workspaceId, permission) {
|
|
|
340
407
|
}
|
|
341
408
|
function requirePermission(grant, permission) {
|
|
342
409
|
if (!hasPermission(grant.permissions, permission)) {
|
|
410
|
+
if (permission === "variable-sets:use") {
|
|
411
|
+
throw new HTTPException2(403, {
|
|
412
|
+
message: "missing permission: variable-sets:use (deprecated alias: environments:use)"
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (permission === "variable-sets:manage") {
|
|
416
|
+
throw new HTTPException2(403, {
|
|
417
|
+
message: "missing permission: variable-sets:manage (deprecated alias: environments:manage)"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
343
420
|
throw new HTTPException2(403, { message: `missing permission: ${permission}` });
|
|
344
421
|
}
|
|
345
422
|
}
|
|
346
423
|
function hasPermission(permissions, permission) {
|
|
347
|
-
|
|
424
|
+
const aliases = {
|
|
425
|
+
"variable-sets:use": ["environments:use"],
|
|
426
|
+
"variable-sets:manage": ["environments:manage"]
|
|
427
|
+
};
|
|
428
|
+
return permissions.includes(permission) || (aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) || permissions.includes("workspace:admin");
|
|
348
429
|
}
|
|
349
430
|
async function resolveAccessContext(c, deps) {
|
|
350
431
|
if (deps.settings.productAccessMode === "local") {
|
|
@@ -364,6 +445,10 @@ async function resolveAccessContext(c, deps) {
|
|
|
364
445
|
if (delegated) {
|
|
365
446
|
return delegated;
|
|
366
447
|
}
|
|
448
|
+
const apiKey = await apiKeyAccessContext(c, deps, "configured");
|
|
449
|
+
if (apiKey) {
|
|
450
|
+
return apiKey;
|
|
451
|
+
}
|
|
367
452
|
if (deps.settings.delegationSecret) {
|
|
368
453
|
return null;
|
|
369
454
|
}
|
|
@@ -384,29 +469,9 @@ async function resolveAccessContext(c, deps) {
|
|
|
384
469
|
if (delegated) {
|
|
385
470
|
return delegated;
|
|
386
471
|
}
|
|
387
|
-
const apiKey = await
|
|
472
|
+
const apiKey = await apiKeyAccessContext(c, deps, "managed");
|
|
388
473
|
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
|
-
};
|
|
474
|
+
return apiKey;
|
|
410
475
|
}
|
|
411
476
|
}
|
|
412
477
|
if (deps.managedAuth) {
|
|
@@ -421,6 +486,44 @@ async function resolveAccessContext(c, deps) {
|
|
|
421
486
|
}
|
|
422
487
|
return null;
|
|
423
488
|
}
|
|
489
|
+
async function apiKeyAccessContext(c, deps, mode) {
|
|
490
|
+
const bearer = bearerToken(c);
|
|
491
|
+
if (!bearer) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const apiKey = await findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
|
|
495
|
+
if (!apiKey) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const subjectId = `api_key:${apiKey.id}`;
|
|
499
|
+
const accountPermissions = apiKey.workspaceId ? apiKey.permissions.filter(
|
|
500
|
+
(permission) => permission === "billing:read" || permission === "billing:manage"
|
|
501
|
+
) : apiKey.permissions;
|
|
502
|
+
return {
|
|
503
|
+
mode,
|
|
504
|
+
subjectId,
|
|
505
|
+
subjectLabel: apiKey.name,
|
|
506
|
+
accountGrants: [
|
|
507
|
+
{
|
|
508
|
+
accountId: apiKey.accountId,
|
|
509
|
+
subjectId,
|
|
510
|
+
subjectLabel: apiKey.name,
|
|
511
|
+
permissions: accountPermissions
|
|
512
|
+
}
|
|
513
|
+
],
|
|
514
|
+
workspaceGrants: apiKey.workspaceId ? [
|
|
515
|
+
{
|
|
516
|
+
workspaceId: apiKey.workspaceId,
|
|
517
|
+
accountId: apiKey.accountId,
|
|
518
|
+
subjectId,
|
|
519
|
+
subjectLabel: apiKey.name,
|
|
520
|
+
permissions: apiKey.permissions
|
|
521
|
+
}
|
|
522
|
+
] : [],
|
|
523
|
+
defaultAccountId: apiKey.accountId,
|
|
524
|
+
defaultWorkspaceId: apiKey.workspaceId
|
|
525
|
+
};
|
|
526
|
+
}
|
|
424
527
|
async function delegatedAccessContext(c, deps, mode, token = bearerToken(c)) {
|
|
425
528
|
if (!token || !deps.settings.delegationSecret) {
|
|
426
529
|
return null;
|
|
@@ -433,22 +536,32 @@ async function delegatedAccessContext(c, deps, mode, token = bearerToken(c)) {
|
|
|
433
536
|
mode,
|
|
434
537
|
subjectId: payload.subjectId,
|
|
435
538
|
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
436
|
-
accountGrants: [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
539
|
+
accountGrants: [
|
|
540
|
+
{
|
|
541
|
+
accountId: payload.accountId,
|
|
542
|
+
subjectId: payload.subjectId,
|
|
543
|
+
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
544
|
+
permissions: payload.permissions
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
workspaceGrants: [
|
|
548
|
+
{
|
|
549
|
+
workspaceId: payload.workspaceId,
|
|
550
|
+
accountId: payload.accountId,
|
|
551
|
+
subjectId: payload.subjectId,
|
|
552
|
+
...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
|
|
553
|
+
permissions: payload.permissions,
|
|
554
|
+
// sessionId is worker-asserted (HMAC-signed token claim), not agent
|
|
555
|
+
// controlled; it scopes session-bound MCP tools such as goal management.
|
|
556
|
+
metadata: {
|
|
557
|
+
delegated: true,
|
|
558
|
+
...payload.sessionId ? { sessionId: payload.sessionId } : {},
|
|
559
|
+
// Caller identity: the turn that minted this token. Tools classify the
|
|
560
|
+
// CALLER from this instead of re-reading the live active pointer.
|
|
561
|
+
...payload.turnId ? { turnId: payload.turnId } : {}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
],
|
|
452
565
|
defaultAccountId: payload.accountId,
|
|
453
566
|
defaultWorkspaceId: payload.workspaceId
|
|
454
567
|
};
|
|
@@ -483,10 +596,17 @@ async function requireLimit(deps, input) {
|
|
|
483
596
|
if (decision.allowed) {
|
|
484
597
|
return;
|
|
485
598
|
}
|
|
486
|
-
throw new HTTPException3(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
599
|
+
throw new HTTPException3(decision.code === "insufficient_credits" ? 402 : 429, {
|
|
600
|
+
message: decision.message
|
|
601
|
+
});
|
|
487
602
|
}
|
|
488
603
|
async function checkLimit(deps, input) {
|
|
489
|
-
const codexBilled = input.workspaceId ? await isCodexBilledTurn({
|
|
604
|
+
const codexBilled = input.workspaceId ? await isCodexBilledTurn({
|
|
605
|
+
db: deps.db,
|
|
606
|
+
settings: deps.settings,
|
|
607
|
+
workspaceId: input.workspaceId,
|
|
608
|
+
model: input.model
|
|
609
|
+
}) : false;
|
|
490
610
|
const creditDecision = await checkCreditBalance(deps, input, codexBilled);
|
|
491
611
|
if (!creditDecision.allowed) {
|
|
492
612
|
return creditDecision;
|
|
@@ -518,7 +638,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
518
638
|
since: startOfUtcMonth()
|
|
519
639
|
});
|
|
520
640
|
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
521
|
-
return blocked(
|
|
641
|
+
return blocked(
|
|
642
|
+
"max_monthly_cost_micros_per_account",
|
|
643
|
+
`monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`
|
|
644
|
+
);
|
|
522
645
|
}
|
|
523
646
|
}
|
|
524
647
|
switch (input.action) {
|
|
@@ -527,27 +650,39 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
527
650
|
return { allowed: true };
|
|
528
651
|
}
|
|
529
652
|
const count = await countWorkspacesForAccount(deps.db, input.accountId);
|
|
530
|
-
return count < limits.maxWorkspacesPerAccount ? { allowed: true } : blocked(
|
|
653
|
+
return count < limits.maxWorkspacesPerAccount ? { allowed: true } : blocked(
|
|
654
|
+
"max_workspaces_per_account",
|
|
655
|
+
`workspace limit reached (${limits.maxWorkspacesPerAccount})`
|
|
656
|
+
);
|
|
531
657
|
}
|
|
532
658
|
case "api_key:create": {
|
|
533
659
|
if (!limits.maxApiKeysPerWorkspace || !input.workspaceId) {
|
|
534
660
|
return { allowed: true };
|
|
535
661
|
}
|
|
536
662
|
const count = await countActiveApiKeysForWorkspace(deps.db, input.workspaceId);
|
|
537
|
-
return count < limits.maxApiKeysPerWorkspace ? { allowed: true } : blocked(
|
|
663
|
+
return count < limits.maxApiKeysPerWorkspace ? { allowed: true } : blocked(
|
|
664
|
+
"max_api_keys_per_workspace",
|
|
665
|
+
`API key limit reached (${limits.maxApiKeysPerWorkspace})`
|
|
666
|
+
);
|
|
538
667
|
}
|
|
539
668
|
case "schedule:create": {
|
|
540
669
|
if (!limits.maxSchedulesPerWorkspace || !input.workspaceId) {
|
|
541
670
|
return { allowed: true };
|
|
542
671
|
}
|
|
543
672
|
const count = await countScheduledTasksForWorkspace(deps.db, input.workspaceId);
|
|
544
|
-
return count < limits.maxSchedulesPerWorkspace ? { allowed: true } : blocked(
|
|
673
|
+
return count < limits.maxSchedulesPerWorkspace ? { allowed: true } : blocked(
|
|
674
|
+
"max_schedules_per_workspace",
|
|
675
|
+
`scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`
|
|
676
|
+
);
|
|
545
677
|
}
|
|
546
678
|
case "file:upload": {
|
|
547
679
|
if (!limits.maxFileUploadBytes || !input.quantity) {
|
|
548
680
|
return { allowed: true };
|
|
549
681
|
}
|
|
550
|
-
return input.quantity <= limits.maxFileUploadBytes ? { allowed: true } : blocked(
|
|
682
|
+
return input.quantity <= limits.maxFileUploadBytes ? { allowed: true } : blocked(
|
|
683
|
+
"max_file_upload_bytes",
|
|
684
|
+
`file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`
|
|
685
|
+
);
|
|
551
686
|
}
|
|
552
687
|
case "agent_run:create": {
|
|
553
688
|
if (!limits.maxMonthlyAgentRunsPerWorkspace || !input.workspaceId) {
|
|
@@ -559,7 +694,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
559
694
|
since: startOfUtcMonth()
|
|
560
695
|
});
|
|
561
696
|
const requested = input.quantity ?? 0;
|
|
562
|
-
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace ? { allowed: true } : blocked(
|
|
697
|
+
return used + requested <= limits.maxMonthlyAgentRunsPerWorkspace ? { allowed: true } : blocked(
|
|
698
|
+
"max_monthly_agent_runs_per_workspace",
|
|
699
|
+
`monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`
|
|
700
|
+
);
|
|
563
701
|
}
|
|
564
702
|
case "tokens:consume": {
|
|
565
703
|
if (codexBilled || !limits.maxMonthlyTokensPerWorkspace || !input.workspaceId) {
|
|
@@ -571,7 +709,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
571
709
|
since: startOfUtcMonth()
|
|
572
710
|
});
|
|
573
711
|
const requested = input.quantity ?? 0;
|
|
574
|
-
return used + requested <= limits.maxMonthlyTokensPerWorkspace ? { allowed: true } : blocked(
|
|
712
|
+
return used + requested <= limits.maxMonthlyTokensPerWorkspace ? { allowed: true } : blocked(
|
|
713
|
+
"max_monthly_tokens_per_workspace",
|
|
714
|
+
`monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`
|
|
715
|
+
);
|
|
575
716
|
}
|
|
576
717
|
case "document:index": {
|
|
577
718
|
if (!limits.maxDocumentIndexedChunksPerWorkspace || !input.workspaceId) {
|
|
@@ -583,7 +724,10 @@ async function checkStaticCaps(deps, input, codexBilled) {
|
|
|
583
724
|
since: startOfUtcMonth()
|
|
584
725
|
});
|
|
585
726
|
const requested = input.quantity ?? 0;
|
|
586
|
-
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace ? { allowed: true } : blocked(
|
|
727
|
+
return used + requested <= limits.maxDocumentIndexedChunksPerWorkspace ? { allowed: true } : blocked(
|
|
728
|
+
"max_document_indexed_chunks_per_workspace",
|
|
729
|
+
`monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`
|
|
730
|
+
);
|
|
587
731
|
}
|
|
588
732
|
}
|
|
589
733
|
}
|
|
@@ -604,7 +748,7 @@ function usesCreditLimits(deps) {
|
|
|
604
748
|
return deps.settings.billingMode === "stripe" || deps.settings.usageLimitsMode === "managed";
|
|
605
749
|
}
|
|
606
750
|
function isCostlyAction(action) {
|
|
607
|
-
return action === "agent_run:create" || action === "tokens:consume" || action === "
|
|
751
|
+
return action === "agent_run:create" || action === "tokens:consume" || action === "document:index";
|
|
608
752
|
}
|
|
609
753
|
function blocked(code, message) {
|
|
610
754
|
return { allowed: false, code, message };
|
|
@@ -623,18 +767,18 @@ import {
|
|
|
623
767
|
CapabilityCatalogItem
|
|
624
768
|
} from "@opengeni/contracts";
|
|
625
769
|
import {
|
|
626
|
-
|
|
770
|
+
decryptVariableSetValue,
|
|
627
771
|
decryptedCapabilityHeaders,
|
|
628
772
|
disableCapabilityInstallation,
|
|
629
773
|
enableCapabilityInstallation,
|
|
630
774
|
enablePackInstallation,
|
|
631
|
-
|
|
775
|
+
encryptVariableSetValue,
|
|
632
776
|
getCapabilityCatalogItem,
|
|
633
777
|
getCapabilityInstallation,
|
|
634
778
|
getConnectionMetadata,
|
|
635
779
|
getPackInstallation,
|
|
636
780
|
getStoredCapabilityHeaderCiphertext,
|
|
637
|
-
|
|
781
|
+
getVariableSet as getVariableSet2,
|
|
638
782
|
listCapabilityCatalogItems,
|
|
639
783
|
listCapabilityInstallations,
|
|
640
784
|
listEnabledMcpCapabilityServers,
|
|
@@ -647,10 +791,7 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
|
647
791
|
|
|
648
792
|
// src/domain/environments.ts
|
|
649
793
|
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
650
|
-
import {
|
|
651
|
-
getWorkspaceEnvironment,
|
|
652
|
-
recordAuditEvent
|
|
653
|
-
} from "@opengeni/db";
|
|
794
|
+
import { getVariableSet, recordAuditEvent } from "@opengeni/db";
|
|
654
795
|
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
655
796
|
var MAX_ENVIRONMENTS_PER_WORKSPACE = 25;
|
|
656
797
|
var MAX_VARIABLES_PER_ENVIRONMENT = 100;
|
|
@@ -671,6 +812,8 @@ var reservedExactNames = /* @__PURE__ */ new Set([
|
|
|
671
812
|
"PERL5LIB",
|
|
672
813
|
"GH_TOKEN",
|
|
673
814
|
"GITHUB_TOKEN",
|
|
815
|
+
"GITLAB_TOKEN",
|
|
816
|
+
"AZURE_DEVOPS_EXT_PAT",
|
|
674
817
|
"GIT_ASKPASS",
|
|
675
818
|
"GIT_TERMINAL_PROMPT"
|
|
676
819
|
]);
|
|
@@ -682,46 +825,52 @@ var reservedPrefixes = [
|
|
|
682
825
|
"LD_",
|
|
683
826
|
"DYLD_"
|
|
684
827
|
];
|
|
685
|
-
function
|
|
828
|
+
function assertAllowedVariableSetVariableName(name) {
|
|
686
829
|
if (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
|
|
687
|
-
throw new HTTPException4(422, {
|
|
830
|
+
throw new HTTPException4(422, {
|
|
831
|
+
message: `reserved variable set variable name / reserved environment variable name: ${name}`
|
|
832
|
+
});
|
|
688
833
|
}
|
|
689
834
|
}
|
|
690
|
-
|
|
835
|
+
var assertAllowedEnvironmentVariableName = assertAllowedVariableSetVariableName;
|
|
836
|
+
function requireVariableSetEncryption(settings) {
|
|
691
837
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
692
838
|
if (!key) {
|
|
693
|
-
throw new HTTPException4(503, {
|
|
839
|
+
throw new HTTPException4(503, {
|
|
840
|
+
message: "variable sets require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
|
|
841
|
+
});
|
|
694
842
|
}
|
|
695
843
|
return key;
|
|
696
844
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
845
|
+
var requireEnvironmentEncryption = requireVariableSetEncryption;
|
|
846
|
+
async function requireVariableSetForApi(db, workspaceId, variableSetId) {
|
|
847
|
+
const variableSet = await getVariableSet(db, workspaceId, variableSetId);
|
|
848
|
+
if (!variableSet) {
|
|
849
|
+
throw new HTTPException4(404, { message: "variableSet not found" });
|
|
701
850
|
}
|
|
702
|
-
return
|
|
851
|
+
return variableSet;
|
|
703
852
|
}
|
|
704
|
-
async function
|
|
705
|
-
|
|
853
|
+
async function validateVariableSetAttachment(deps, grant, workspaceId, variableSetId, options = {}) {
|
|
854
|
+
requireVariableSetEncryption(deps.settings);
|
|
706
855
|
if (!options.preauthorized) {
|
|
707
|
-
requirePermission(grant, "
|
|
856
|
+
requirePermission(grant, "variable-sets:use");
|
|
708
857
|
}
|
|
709
|
-
const
|
|
710
|
-
if (!
|
|
711
|
-
throw new HTTPException4(422, { message: "unknown
|
|
858
|
+
const variableSet = await getVariableSet(deps.db, workspaceId, variableSetId);
|
|
859
|
+
if (!variableSet) {
|
|
860
|
+
throw new HTTPException4(422, { message: "unknown variableSetId" });
|
|
712
861
|
}
|
|
713
|
-
return
|
|
862
|
+
return variableSet;
|
|
714
863
|
}
|
|
715
|
-
async function
|
|
864
|
+
async function recordVariableSetAuditEvent(db, input) {
|
|
716
865
|
await recordAuditEvent(db, {
|
|
717
866
|
accountId: input.grant.accountId,
|
|
718
867
|
workspaceId: input.grant.workspaceId,
|
|
719
868
|
subjectId: input.grant.subjectId,
|
|
720
869
|
action: input.action,
|
|
721
|
-
targetType: "
|
|
722
|
-
targetId: input.
|
|
870
|
+
targetType: "workspace_variable_set",
|
|
871
|
+
targetId: input.variableSetId,
|
|
723
872
|
metadata: {
|
|
724
|
-
|
|
873
|
+
variableSetId: input.variableSetId,
|
|
725
874
|
...input.variableName ? { name: input.variableName } : {}
|
|
726
875
|
}
|
|
727
876
|
});
|
|
@@ -731,7 +880,11 @@ async function recordEnvironmentAuditEvent(db, input) {
|
|
|
731
880
|
import {
|
|
732
881
|
CapabilityPack
|
|
733
882
|
} from "@opengeni/contracts";
|
|
734
|
-
import {
|
|
883
|
+
import {
|
|
884
|
+
getWorkspacePack,
|
|
885
|
+
listPackInstallations,
|
|
886
|
+
listWorkspacePacks
|
|
887
|
+
} from "@opengeni/db";
|
|
735
888
|
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
736
889
|
var MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
|
|
737
890
|
var marketingSocialPack = {
|
|
@@ -780,7 +933,12 @@ var marketingSocialPack = {
|
|
|
780
933
|
category: "social-media",
|
|
781
934
|
authModel: "oauth2_authorization_code",
|
|
782
935
|
providers: ["instagram", "facebook"],
|
|
783
|
-
scopes: [
|
|
936
|
+
scopes: [
|
|
937
|
+
"instagram_basic",
|
|
938
|
+
"instagram_manage_insights",
|
|
939
|
+
"pages_read_engagement",
|
|
940
|
+
"pages_show_list"
|
|
941
|
+
],
|
|
784
942
|
required: false,
|
|
785
943
|
metadata: {
|
|
786
944
|
docs: "https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/"
|
|
@@ -804,7 +962,10 @@ var marketingSocialPack = {
|
|
|
804
962
|
category: "social-media",
|
|
805
963
|
authModel: "oauth2_authorization_code",
|
|
806
964
|
providers: ["youtube"],
|
|
807
|
-
scopes: [
|
|
965
|
+
scopes: [
|
|
966
|
+
"https://www.googleapis.com/auth/youtube.readonly",
|
|
967
|
+
"https://www.googleapis.com/auth/yt-analytics.readonly"
|
|
968
|
+
],
|
|
808
969
|
required: false,
|
|
809
970
|
metadata: {
|
|
810
971
|
docs: "https://developers.google.com/youtube/v3"
|
|
@@ -957,16 +1118,24 @@ async function buildCapabilityCatalog(input) {
|
|
|
957
1118
|
listWorkspaceCapabilityPacks(input.db, input.workspaceId),
|
|
958
1119
|
discoverBundledSkills()
|
|
959
1120
|
]);
|
|
960
|
-
const capabilityInstallationById = new Map(
|
|
961
|
-
|
|
1121
|
+
const capabilityInstallationById = new Map(
|
|
1122
|
+
capabilityInstallations.map((installation) => [installation.capabilityId, installation])
|
|
1123
|
+
);
|
|
1124
|
+
const activePackIds = new Set(
|
|
1125
|
+
packInstallations.filter((installation) => installation.status === "active").map((installation) => installation.packId)
|
|
1126
|
+
);
|
|
962
1127
|
const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
|
|
963
1128
|
const builtIns = [
|
|
964
|
-
...workspacePacks.map(
|
|
1129
|
+
...workspacePacks.map(
|
|
1130
|
+
(pack) => packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual")
|
|
1131
|
+
),
|
|
965
1132
|
...configuredMcpCatalogItems(input.settings),
|
|
966
1133
|
...platformApiCatalogItems(),
|
|
967
1134
|
...bundledSkills
|
|
968
1135
|
];
|
|
969
|
-
const items = dedupeCatalogItems([...builtIns, ...persistedItems]).map(
|
|
1136
|
+
const items = dedupeCatalogItems([...builtIns, ...persistedItems]).map(
|
|
1137
|
+
(item) => applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds)
|
|
1138
|
+
).sort(compareCatalogItems);
|
|
970
1139
|
return {
|
|
971
1140
|
items,
|
|
972
1141
|
installations: capabilityInstallations
|
|
@@ -975,7 +1144,9 @@ async function buildCapabilityCatalog(input) {
|
|
|
975
1144
|
async function createCatalogItem(input) {
|
|
976
1145
|
const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
|
|
977
1146
|
if (id.startsWith("pack:")) {
|
|
978
|
-
throw new HTTPException6(422, {
|
|
1147
|
+
throw new HTTPException6(422, {
|
|
1148
|
+
message: "packs are managed by OpenGeni and cannot be manually created"
|
|
1149
|
+
});
|
|
979
1150
|
}
|
|
980
1151
|
const source = input.payload.source === "built_in" || input.payload.source === "configured" || input.payload.source === "registry" ? "manual" : input.payload.source;
|
|
981
1152
|
const metadata = {
|
|
@@ -1000,9 +1171,16 @@ async function createCatalogItem(input) {
|
|
|
1000
1171
|
});
|
|
1001
1172
|
}
|
|
1002
1173
|
async function enableCapability(input) {
|
|
1003
|
-
const item = await requireCatalogItem(
|
|
1174
|
+
const item = await requireCatalogItem(
|
|
1175
|
+
input.db,
|
|
1176
|
+
input.workspaceId,
|
|
1177
|
+
input.settings,
|
|
1178
|
+
input.capabilityId
|
|
1179
|
+
);
|
|
1004
1180
|
if (item.kind === "mcp" && !item.runtime.available) {
|
|
1005
|
-
throw new HTTPException6(422, {
|
|
1181
|
+
throw new HTTPException6(422, {
|
|
1182
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled"
|
|
1183
|
+
});
|
|
1006
1184
|
}
|
|
1007
1185
|
let installationMetadata = input.payload.metadata;
|
|
1008
1186
|
const installationConfig = { ...input.payload.config };
|
|
@@ -1024,7 +1202,7 @@ async function enableCapability(input) {
|
|
|
1024
1202
|
if (headers) {
|
|
1025
1203
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
1026
1204
|
installationConfig.headersEncrypted = Object.fromEntries(
|
|
1027
|
-
Object.entries(headers).map(([name, value]) => [name,
|
|
1205
|
+
Object.entries(headers).map(([name, value]) => [name, encryptVariableSetValue(key, value)])
|
|
1028
1206
|
);
|
|
1029
1207
|
}
|
|
1030
1208
|
}
|
|
@@ -1036,36 +1214,44 @@ async function enableCapability(input) {
|
|
|
1036
1214
|
}
|
|
1037
1215
|
await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
|
|
1038
1216
|
const existing = await getPackInstallation(input.db, input.workspaceId, packId);
|
|
1039
|
-
const
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
if (pack.
|
|
1217
|
+
const storedVariableSetId = typeof existing?.metadata.variableSetId === "string" ? existing.metadata.variableSetId : typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
|
|
1218
|
+
const requestedVariableSetId = input.payload.variableSetId;
|
|
1219
|
+
const variableSetId = requestedVariableSetId ?? storedVariableSetId;
|
|
1220
|
+
if (pack.variableSet?.required && !variableSetId) {
|
|
1043
1221
|
throw new HTTPException6(422, {
|
|
1044
|
-
message: `pack ${packId} requires an
|
|
1222
|
+
message: `pack ${packId} requires an variableSet attachment; pass variableSetId`
|
|
1045
1223
|
});
|
|
1046
1224
|
}
|
|
1047
|
-
if (
|
|
1048
|
-
if (
|
|
1049
|
-
const
|
|
1225
|
+
if (variableSetId) {
|
|
1226
|
+
if (requestedVariableSetId) {
|
|
1227
|
+
const variableSet = await validateVariableSetAttachment(
|
|
1050
1228
|
{ settings: input.settings, db: input.db },
|
|
1051
1229
|
input.grant,
|
|
1052
1230
|
input.workspaceId,
|
|
1053
|
-
|
|
1231
|
+
requestedVariableSetId
|
|
1232
|
+
);
|
|
1233
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
1234
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name)
|
|
1054
1235
|
);
|
|
1055
|
-
const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
1056
1236
|
if (missing.length > 0) {
|
|
1057
|
-
throw new HTTPException6(422, {
|
|
1237
|
+
throw new HTTPException6(422, {
|
|
1238
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`
|
|
1239
|
+
});
|
|
1058
1240
|
}
|
|
1059
1241
|
} else {
|
|
1060
|
-
const
|
|
1061
|
-
if (!
|
|
1242
|
+
const variableSet = await getVariableSet2(input.db, input.workspaceId, variableSetId);
|
|
1243
|
+
if (!variableSet) {
|
|
1062
1244
|
throw new HTTPException6(422, {
|
|
1063
|
-
message: `the stored
|
|
1245
|
+
message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`
|
|
1064
1246
|
});
|
|
1065
1247
|
}
|
|
1066
|
-
const missing = (pack.
|
|
1248
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
1249
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name)
|
|
1250
|
+
);
|
|
1067
1251
|
if (missing.length > 0) {
|
|
1068
|
-
throw new HTTPException6(422, {
|
|
1252
|
+
throw new HTTPException6(422, {
|
|
1253
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`
|
|
1254
|
+
});
|
|
1069
1255
|
}
|
|
1070
1256
|
}
|
|
1071
1257
|
}
|
|
@@ -1076,7 +1262,7 @@ async function enableCapability(input) {
|
|
|
1076
1262
|
metadata: {
|
|
1077
1263
|
...input.payload.metadata,
|
|
1078
1264
|
packVersion: pack.version,
|
|
1079
|
-
...
|
|
1265
|
+
...variableSetId ? { variableSetId } : {}
|
|
1080
1266
|
}
|
|
1081
1267
|
});
|
|
1082
1268
|
}
|
|
@@ -1095,13 +1281,22 @@ async function resolveMcpCredentialHeaders(input, item) {
|
|
|
1095
1281
|
requireCapabilityHeaderEncryption(input.settings);
|
|
1096
1282
|
return provided;
|
|
1097
1283
|
}
|
|
1098
|
-
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
1284
|
+
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
1285
|
+
input.db,
|
|
1286
|
+
input.workspaceId,
|
|
1287
|
+
item.id
|
|
1288
|
+
);
|
|
1099
1289
|
if (!storedCiphertext) {
|
|
1100
1290
|
return null;
|
|
1101
1291
|
}
|
|
1102
1292
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
1103
1293
|
try {
|
|
1104
|
-
return Object.fromEntries(
|
|
1294
|
+
return Object.fromEntries(
|
|
1295
|
+
Object.entries(storedCiphertext).map(([name, value]) => [
|
|
1296
|
+
name,
|
|
1297
|
+
decryptVariableSetValue(key, value)
|
|
1298
|
+
])
|
|
1299
|
+
);
|
|
1105
1300
|
} catch {
|
|
1106
1301
|
throw new HTTPException6(422, {
|
|
1107
1302
|
message: `stored credential headers for "${item.name}" could not be decrypted; supply them again in the enable request "headers" field`
|
|
@@ -1114,7 +1309,9 @@ function normalizedMcpCredentialHeaders(headers) {
|
|
|
1114
1309
|
return null;
|
|
1115
1310
|
}
|
|
1116
1311
|
if (entries.length > maxMcpCredentialHeaders) {
|
|
1117
|
-
throw new HTTPException6(422, {
|
|
1312
|
+
throw new HTTPException6(422, {
|
|
1313
|
+
message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers`
|
|
1314
|
+
});
|
|
1118
1315
|
}
|
|
1119
1316
|
const seen = /* @__PURE__ */ new Set();
|
|
1120
1317
|
for (const [name, value] of entries) {
|
|
@@ -1127,17 +1324,23 @@ function normalizedMcpCredentialHeaders(headers) {
|
|
|
1127
1324
|
}
|
|
1128
1325
|
seen.add(lower);
|
|
1129
1326
|
if (value.length === 0 || value.length > maxMcpCredentialHeaderValueLength) {
|
|
1130
|
-
throw new HTTPException6(422, {
|
|
1327
|
+
throw new HTTPException6(422, {
|
|
1328
|
+
message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters`
|
|
1329
|
+
});
|
|
1131
1330
|
}
|
|
1132
1331
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
1133
|
-
throw new HTTPException6(422, {
|
|
1332
|
+
throw new HTTPException6(422, {
|
|
1333
|
+
message: `credential header ${name} contains forbidden control characters`
|
|
1334
|
+
});
|
|
1134
1335
|
}
|
|
1135
1336
|
}
|
|
1136
1337
|
return Object.fromEntries(entries);
|
|
1137
1338
|
}
|
|
1138
1339
|
async function validateMcpCapabilityConnectionRef(input, item, ref) {
|
|
1139
1340
|
if (ref.subjectScope === "subject") {
|
|
1140
|
-
throw new HTTPException6(422, {
|
|
1341
|
+
throw new HTTPException6(422, {
|
|
1342
|
+
message: "subject-owned connection refs are not supported for agent runtime use yet"
|
|
1343
|
+
});
|
|
1141
1344
|
}
|
|
1142
1345
|
const normalized = {
|
|
1143
1346
|
providerDomain: ref.providerDomain.trim(),
|
|
@@ -1151,26 +1354,43 @@ async function validateMcpCapabilityConnectionRef(input, item, ref) {
|
|
|
1151
1354
|
throw new HTTPException6(422, { message: "connectionRef.providerDomain is required" });
|
|
1152
1355
|
}
|
|
1153
1356
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
1154
|
-
throw new HTTPException6(422, {
|
|
1357
|
+
throw new HTTPException6(422, {
|
|
1358
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef"
|
|
1359
|
+
});
|
|
1155
1360
|
}
|
|
1156
1361
|
if (!normalized.connectionId) {
|
|
1157
1362
|
return normalized;
|
|
1158
1363
|
}
|
|
1159
|
-
const connection = await getConnectionMetadata(
|
|
1364
|
+
const connection = await getConnectionMetadata(
|
|
1365
|
+
input.db,
|
|
1366
|
+
input.workspaceId,
|
|
1367
|
+
normalized.connectionId,
|
|
1368
|
+
input.grant.subjectId
|
|
1369
|
+
);
|
|
1160
1370
|
if (!connection) {
|
|
1161
|
-
throw new HTTPException6(422, {
|
|
1371
|
+
throw new HTTPException6(422, {
|
|
1372
|
+
message: "connectionRef.connectionId does not reference a visible connection"
|
|
1373
|
+
});
|
|
1162
1374
|
}
|
|
1163
1375
|
if (connection.subjectId !== null) {
|
|
1164
|
-
throw new HTTPException6(422, {
|
|
1376
|
+
throw new HTTPException6(422, {
|
|
1377
|
+
message: "agent runtime connection refs must reference workspace-shared connections in I1"
|
|
1378
|
+
});
|
|
1165
1379
|
}
|
|
1166
1380
|
if (connection.status !== "active") {
|
|
1167
|
-
throw new HTTPException6(422, {
|
|
1381
|
+
throw new HTTPException6(422, {
|
|
1382
|
+
message: `connectionRef.connectionId is not active (${connection.status})`
|
|
1383
|
+
});
|
|
1168
1384
|
}
|
|
1169
1385
|
if (connection.providerDomain !== normalized.providerDomain) {
|
|
1170
|
-
throw new HTTPException6(422, {
|
|
1386
|
+
throw new HTTPException6(422, {
|
|
1387
|
+
message: "connectionRef.providerDomain does not match the referenced connection"
|
|
1388
|
+
});
|
|
1171
1389
|
}
|
|
1172
1390
|
if (normalized.kind && connection.kind !== normalized.kind) {
|
|
1173
|
-
throw new HTTPException6(422, {
|
|
1391
|
+
throw new HTTPException6(422, {
|
|
1392
|
+
message: "connectionRef.kind does not match the referenced connection"
|
|
1393
|
+
});
|
|
1174
1394
|
}
|
|
1175
1395
|
return normalized;
|
|
1176
1396
|
}
|
|
@@ -1210,7 +1430,9 @@ function requiredCapabilityHeaders(metadata) {
|
|
|
1210
1430
|
function requireCapabilityHeaderEncryption(settings) {
|
|
1211
1431
|
const key = environmentsEncryptionKeyBytes2(settings);
|
|
1212
1432
|
if (!key) {
|
|
1213
|
-
throw new HTTPException6(503, {
|
|
1433
|
+
throw new HTTPException6(503, {
|
|
1434
|
+
message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
|
|
1435
|
+
});
|
|
1214
1436
|
}
|
|
1215
1437
|
return key;
|
|
1216
1438
|
}
|
|
@@ -1219,7 +1441,9 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
|
|
|
1219
1441
|
return {};
|
|
1220
1442
|
}
|
|
1221
1443
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
1222
|
-
throw new HTTPException6(422, {
|
|
1444
|
+
throw new HTTPException6(422, {
|
|
1445
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled"
|
|
1446
|
+
});
|
|
1223
1447
|
}
|
|
1224
1448
|
try {
|
|
1225
1449
|
const result = await probe({
|
|
@@ -1238,14 +1462,17 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
|
|
|
1238
1462
|
};
|
|
1239
1463
|
} catch (error) {
|
|
1240
1464
|
throw new HTTPException6(422, {
|
|
1241
|
-
message: `MCP capability "${item.name}" could not be enabled because
|
|
1465
|
+
message: `MCP capability "${item.name}" could not be enabled because ${mcpProbeErrorMessage(error, item.endpointUrl)}`
|
|
1242
1466
|
});
|
|
1243
1467
|
}
|
|
1244
1468
|
}
|
|
1245
1469
|
async function probeStreamableHttpMcpServer(input) {
|
|
1246
1470
|
const controller = new AbortController();
|
|
1247
1471
|
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
1248
|
-
const client = new Client(
|
|
1472
|
+
const client = new Client(
|
|
1473
|
+
{ name: "opengeni-capability-probe", version: "0.1.0" },
|
|
1474
|
+
{ capabilities: {} }
|
|
1475
|
+
);
|
|
1249
1476
|
try {
|
|
1250
1477
|
const transport = new StreamableHTTPClientTransport(new URL(input.url), {
|
|
1251
1478
|
requestInit: {
|
|
@@ -1253,25 +1480,49 @@ async function probeStreamableHttpMcpServer(input) {
|
|
|
1253
1480
|
...input.headers ? { headers: input.headers } : {}
|
|
1254
1481
|
}
|
|
1255
1482
|
});
|
|
1256
|
-
await client.connect(transport, {
|
|
1257
|
-
|
|
1483
|
+
await client.connect(transport, {
|
|
1484
|
+
timeout: input.timeoutMs,
|
|
1485
|
+
maxTotalTimeout: input.timeoutMs
|
|
1486
|
+
});
|
|
1487
|
+
const tools = await client.listTools(void 0, {
|
|
1488
|
+
timeout: input.timeoutMs,
|
|
1489
|
+
maxTotalTimeout: input.timeoutMs
|
|
1490
|
+
});
|
|
1258
1491
|
return { toolCount: tools.tools.length };
|
|
1259
1492
|
} finally {
|
|
1260
1493
|
clearTimeout(timeout);
|
|
1261
1494
|
await client.close().catch(() => void 0);
|
|
1262
1495
|
}
|
|
1263
1496
|
}
|
|
1264
|
-
function mcpProbeErrorMessage(error) {
|
|
1497
|
+
function mcpProbeErrorMessage(error, endpointUrl) {
|
|
1265
1498
|
const message = error instanceof Error ? error.message : String(error);
|
|
1266
|
-
|
|
1499
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
1500
|
+
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(
|
|
1501
|
+
normalized
|
|
1502
|
+
)) {
|
|
1503
|
+
return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
|
|
1504
|
+
}
|
|
1505
|
+
return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
|
|
1267
1506
|
}
|
|
1268
1507
|
async function disableCapability(input) {
|
|
1269
|
-
const item = await requireCatalogItem(
|
|
1508
|
+
const item = await requireCatalogItem(
|
|
1509
|
+
input.db,
|
|
1510
|
+
input.workspaceId,
|
|
1511
|
+
input.settings,
|
|
1512
|
+
input.capabilityId
|
|
1513
|
+
);
|
|
1270
1514
|
if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
|
|
1271
|
-
throw new HTTPException6(409, {
|
|
1515
|
+
throw new HTTPException6(409, {
|
|
1516
|
+
message: "built-in and configured capabilities are always available; remove them from configuration to disable them"
|
|
1517
|
+
});
|
|
1272
1518
|
}
|
|
1273
1519
|
if (item.kind === "pack") {
|
|
1274
|
-
await updatePackInstallationStatus(
|
|
1520
|
+
await updatePackInstallationStatus(
|
|
1521
|
+
input.db,
|
|
1522
|
+
input.workspaceId,
|
|
1523
|
+
packIdFromCapabilityId(item.id),
|
|
1524
|
+
"disabled"
|
|
1525
|
+
).catch(() => void 0);
|
|
1275
1526
|
if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
|
|
1276
1527
|
await enableCapabilityInstallation(input.db, {
|
|
1277
1528
|
accountId: input.accountId,
|
|
@@ -1302,16 +1553,18 @@ function settingsWithMcpCapabilityServers(settings, enabled) {
|
|
|
1302
1553
|
if (headers === "unavailable" && !server.connectionRef) {
|
|
1303
1554
|
return [];
|
|
1304
1555
|
}
|
|
1305
|
-
return [
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1556
|
+
return [
|
|
1557
|
+
{
|
|
1558
|
+
id: server.id,
|
|
1559
|
+
name: server.name,
|
|
1560
|
+
url: server.url,
|
|
1561
|
+
...server.allowedTools ? { allowedTools: server.allowedTools } : {},
|
|
1562
|
+
...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
|
|
1563
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
1564
|
+
...headers && headers !== "unavailable" ? { headers } : {},
|
|
1565
|
+
...server.connectionRef ? { connectionRef: server.connectionRef } : {}
|
|
1566
|
+
}
|
|
1567
|
+
];
|
|
1315
1568
|
});
|
|
1316
1569
|
return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
|
|
1317
1570
|
}
|
|
@@ -1365,7 +1618,10 @@ async function discoverMcpRegistryCapabilities(input) {
|
|
|
1365
1618
|
async function fetchMcpRegistryPage(url, options = {}) {
|
|
1366
1619
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1367
1620
|
const controller = new AbortController();
|
|
1368
|
-
const timeout = setTimeout(
|
|
1621
|
+
const timeout = setTimeout(
|
|
1622
|
+
() => controller.abort(),
|
|
1623
|
+
options.timeoutMs ?? mcpRegistryFetchTimeoutMs
|
|
1624
|
+
);
|
|
1369
1625
|
try {
|
|
1370
1626
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
1371
1627
|
if (!response.ok) {
|
|
@@ -1422,28 +1678,30 @@ function packCatalogItem(pack, source) {
|
|
|
1422
1678
|
});
|
|
1423
1679
|
}
|
|
1424
1680
|
function configuredMcpCatalogItems(settings) {
|
|
1425
|
-
return settings.mcpServers.map(
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1681
|
+
return settings.mcpServers.map(
|
|
1682
|
+
(server) => CapabilityCatalogItem.parse({
|
|
1683
|
+
id: `mcp:${server.id}`,
|
|
1684
|
+
kind: "mcp",
|
|
1685
|
+
source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
|
|
1686
|
+
name: server.name ?? server.id,
|
|
1687
|
+
description: firstPartyMcpDescription(server.id),
|
|
1688
|
+
category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
|
|
1689
|
+
tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
|
|
1690
|
+
endpointUrl: server.url,
|
|
1691
|
+
tools: [{ kind: "mcp", id: server.id }],
|
|
1692
|
+
runtime: {
|
|
1693
|
+
available: true,
|
|
1694
|
+
mcpServerId: server.id,
|
|
1695
|
+
transport: "streamable-http",
|
|
1696
|
+
notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
|
|
1697
|
+
},
|
|
1698
|
+
metadata: {
|
|
1699
|
+
mcpServerId: server.id,
|
|
1700
|
+
allowedTools: server.allowedTools ?? [],
|
|
1701
|
+
cacheToolsList: server.cacheToolsList
|
|
1702
|
+
}
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1447
1705
|
}
|
|
1448
1706
|
function platformApiCatalogItems() {
|
|
1449
1707
|
return [
|
|
@@ -1479,46 +1737,56 @@ function platformApiCatalogItems() {
|
|
|
1479
1737
|
tags: ["api", "schedules", "agents"],
|
|
1480
1738
|
endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks"
|
|
1481
1739
|
}
|
|
1482
|
-
].map(
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1740
|
+
].map(
|
|
1741
|
+
(item) => CapabilityCatalogItem.parse({
|
|
1742
|
+
id: item.id,
|
|
1743
|
+
name: item.name,
|
|
1744
|
+
description: item.description,
|
|
1745
|
+
category: item.category,
|
|
1746
|
+
tags: item.tags,
|
|
1747
|
+
kind: "api",
|
|
1748
|
+
source: "built_in",
|
|
1749
|
+
runtime: {
|
|
1750
|
+
available: true,
|
|
1751
|
+
notes: "Available through the OpenGeni API."
|
|
1752
|
+
},
|
|
1753
|
+
metadata: {
|
|
1754
|
+
endpointPath: item.endpointPath
|
|
1755
|
+
}
|
|
1756
|
+
})
|
|
1757
|
+
);
|
|
1498
1758
|
}
|
|
1499
1759
|
async function discoverBundledSkills() {
|
|
1500
|
-
const skillsDir = new URL(
|
|
1760
|
+
const skillsDir = new URL(
|
|
1761
|
+
"../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
|
|
1762
|
+
import.meta.url
|
|
1763
|
+
);
|
|
1501
1764
|
try {
|
|
1502
1765
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1503
|
-
const skills = await Promise.all(
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1766
|
+
const skills = await Promise.all(
|
|
1767
|
+
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
1768
|
+
const skill = await readSkillMetadata(
|
|
1769
|
+
new URL(`${entry.name}/SKILL.md`, skillsDir),
|
|
1770
|
+
entry.name
|
|
1771
|
+
);
|
|
1772
|
+
return CapabilityCatalogItem.parse({
|
|
1773
|
+
id: `skill:${entry.name}`,
|
|
1774
|
+
kind: "skill",
|
|
1775
|
+
source: "built_in",
|
|
1776
|
+
name: skill.name,
|
|
1777
|
+
description: skill.description,
|
|
1778
|
+
category: skill.category,
|
|
1779
|
+
tags: ["skill", skill.category],
|
|
1780
|
+
runtime: {
|
|
1781
|
+
available: true,
|
|
1782
|
+
notes: "Bundled into the sandbox skill library."
|
|
1783
|
+
},
|
|
1784
|
+
metadata: {
|
|
1785
|
+
path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
})
|
|
1789
|
+
);
|
|
1522
1790
|
return skills;
|
|
1523
1791
|
} catch {
|
|
1524
1792
|
return [];
|
|
@@ -1595,7 +1863,11 @@ function firstPartyMcpDescription(id) {
|
|
|
1595
1863
|
return null;
|
|
1596
1864
|
}
|
|
1597
1865
|
function generatedCapabilityId(payload) {
|
|
1598
|
-
const source = [
|
|
1866
|
+
const source = [
|
|
1867
|
+
payload.kind,
|
|
1868
|
+
payload.name,
|
|
1869
|
+
payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""
|
|
1870
|
+
].join(":");
|
|
1599
1871
|
return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
|
|
1600
1872
|
}
|
|
1601
1873
|
function publicRegistryCapabilityId(name, version, endpointUrl) {
|
|
@@ -1633,7 +1905,9 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1633
1905
|
if (official?.isLatest === false) {
|
|
1634
1906
|
return null;
|
|
1635
1907
|
}
|
|
1636
|
-
const remote = server.remotes?.find(
|
|
1908
|
+
const remote = server.remotes?.find(
|
|
1909
|
+
(candidate) => candidate.type === "streamable-http" && candidate.url
|
|
1910
|
+
);
|
|
1637
1911
|
const endpointUrl = validUrl(remote?.url);
|
|
1638
1912
|
if (!remote || !endpointUrl) {
|
|
1639
1913
|
return null;
|
|
@@ -1650,7 +1924,12 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1650
1924
|
name: server.title || server.name,
|
|
1651
1925
|
description: server.description ?? null,
|
|
1652
1926
|
category: "public-mcp",
|
|
1653
|
-
tags: [
|
|
1927
|
+
tags: [
|
|
1928
|
+
"mcp",
|
|
1929
|
+
"public",
|
|
1930
|
+
"registry",
|
|
1931
|
+
...requiredHeaders.length ? ["requires-credentials"] : []
|
|
1932
|
+
],
|
|
1654
1933
|
homepageUrl,
|
|
1655
1934
|
endpointUrl,
|
|
1656
1935
|
installUrl: homepageUrl,
|
|
@@ -1728,6 +2007,367 @@ function storedConnectionRef(config) {
|
|
|
1728
2007
|
return !!ref && typeof ref === "object" && !Array.isArray(ref) && typeof ref.providerDomain === "string";
|
|
1729
2008
|
}
|
|
1730
2009
|
|
|
2010
|
+
// src/rigs/index.ts
|
|
2011
|
+
import {
|
|
2012
|
+
activateRigVersion,
|
|
2013
|
+
countRigs,
|
|
2014
|
+
createRig,
|
|
2015
|
+
createRigChange,
|
|
2016
|
+
createRigVersion,
|
|
2017
|
+
createRigVersionForChangePromotion,
|
|
2018
|
+
deleteRigIfNoActiveSessions,
|
|
2019
|
+
getRig,
|
|
2020
|
+
getRigByName,
|
|
2021
|
+
getRigChange,
|
|
2022
|
+
getRigVersion,
|
|
2023
|
+
getVariableSet as getVariableSet3,
|
|
2024
|
+
listRigChanges,
|
|
2025
|
+
listRigVersions,
|
|
2026
|
+
recordAuditEvent as recordAuditEvent2,
|
|
2027
|
+
RigActiveVersionChangedError,
|
|
2028
|
+
RigChangeTransitionError,
|
|
2029
|
+
updateRig
|
|
2030
|
+
} from "@opengeni/db";
|
|
2031
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
2032
|
+
var MAX_RIGS_PER_WORKSPACE = 50;
|
|
2033
|
+
var MAX_CHECKS_PER_RIG = 100;
|
|
2034
|
+
var MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
|
|
2035
|
+
var MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
|
|
2036
|
+
async function recordRigAuditEvent(db, input) {
|
|
2037
|
+
await recordAuditEvent2(db, {
|
|
2038
|
+
accountId: input.grant.accountId,
|
|
2039
|
+
workspaceId: input.grant.workspaceId,
|
|
2040
|
+
subjectId: input.grant.subjectId,
|
|
2041
|
+
action: input.action,
|
|
2042
|
+
targetType: "rig",
|
|
2043
|
+
targetId: input.rigId,
|
|
2044
|
+
metadata: { rigId: input.rigId, ...input.metadata ?? {} }
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
function rigActorForGrant(grant) {
|
|
2048
|
+
return `user:${grant.subjectId}`;
|
|
2049
|
+
}
|
|
2050
|
+
async function requireRigForApi(db, workspaceId, rigId) {
|
|
2051
|
+
const rig = await getRig(db, workspaceId, rigId);
|
|
2052
|
+
if (!rig) {
|
|
2053
|
+
throw new HTTPException7(404, { message: "rig not found" });
|
|
2054
|
+
}
|
|
2055
|
+
return rig;
|
|
2056
|
+
}
|
|
2057
|
+
async function requireRigChangeForApi(db, workspaceId, rigId, changeId) {
|
|
2058
|
+
const change = await getRigChange(db, workspaceId, changeId);
|
|
2059
|
+
if (!change || change.rigId !== rigId) {
|
|
2060
|
+
throw new HTTPException7(404, { message: "rig change not found" });
|
|
2061
|
+
}
|
|
2062
|
+
return change;
|
|
2063
|
+
}
|
|
2064
|
+
function trimmedRigName(name) {
|
|
2065
|
+
const trimmed = name.trim();
|
|
2066
|
+
if (!trimmed) {
|
|
2067
|
+
throw new HTTPException7(422, { message: "rig name is required" });
|
|
2068
|
+
}
|
|
2069
|
+
return trimmed;
|
|
2070
|
+
}
|
|
2071
|
+
function assertUniqueCheckNames(checks) {
|
|
2072
|
+
if (!checks) {
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2076
|
+
for (const check of checks) {
|
|
2077
|
+
if (seen.has(check.name)) {
|
|
2078
|
+
throw new HTTPException7(422, { message: `duplicate rig check name: ${check.name}` });
|
|
2079
|
+
}
|
|
2080
|
+
seen.add(check.name);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
async function assertVariableSetsExist(db, workspaceId, ids) {
|
|
2084
|
+
if (!ids || ids.length === 0) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const unique = [...new Set(ids)];
|
|
2088
|
+
for (const id of unique) {
|
|
2089
|
+
const variableSet = await getVariableSet3(db, workspaceId, id);
|
|
2090
|
+
if (!variableSet) {
|
|
2091
|
+
throw new HTTPException7(422, { message: `unknown defaultVariableSetId: ${id}` });
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
async function createRigForApi(deps, grant, payload) {
|
|
2096
|
+
const workspaceId = grant.workspaceId;
|
|
2097
|
+
const name = trimmedRigName(payload.name);
|
|
2098
|
+
assertUniqueCheckNames(payload.checks);
|
|
2099
|
+
await assertVariableSetsExist(deps.db, workspaceId, payload.defaultVariableSetIds);
|
|
2100
|
+
if (await countRigs(deps.db, workspaceId) >= MAX_RIGS_PER_WORKSPACE) {
|
|
2101
|
+
throw new HTTPException7(422, {
|
|
2102
|
+
message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
if (await getRigByName(deps.db, workspaceId, name)) {
|
|
2106
|
+
throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
|
|
2107
|
+
}
|
|
2108
|
+
const createdBy = rigActorForGrant(grant);
|
|
2109
|
+
const rig = await createRig(deps.db, {
|
|
2110
|
+
accountId: grant.accountId,
|
|
2111
|
+
workspaceId,
|
|
2112
|
+
name,
|
|
2113
|
+
description: payload.description ?? null,
|
|
2114
|
+
createdBy,
|
|
2115
|
+
initialVersion: {
|
|
2116
|
+
image: payload.image ?? null,
|
|
2117
|
+
setupScript: payload.setupScript ?? null,
|
|
2118
|
+
checks: payload.checks,
|
|
2119
|
+
credentialHooks: payload.credentialHooks,
|
|
2120
|
+
defaultVariableSetIds: payload.defaultVariableSetIds,
|
|
2121
|
+
changelog: "Initial version",
|
|
2122
|
+
createdBy
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.created", rigId: rig.id });
|
|
2126
|
+
return rig;
|
|
2127
|
+
}
|
|
2128
|
+
async function updateRigForApi(deps, grant, rig, payload) {
|
|
2129
|
+
const workspaceId = grant.workspaceId;
|
|
2130
|
+
const name = payload.name !== void 0 ? trimmedRigName(payload.name) : void 0;
|
|
2131
|
+
if (name !== void 0 && name !== rig.name) {
|
|
2132
|
+
const existing = await getRigByName(deps.db, workspaceId, name);
|
|
2133
|
+
if (existing && existing.id !== rig.id) {
|
|
2134
|
+
throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
const updated = await updateRig(deps.db, workspaceId, rig.id, {
|
|
2138
|
+
...name !== void 0 ? { name } : {},
|
|
2139
|
+
...payload.description !== void 0 ? { description: payload.description } : {}
|
|
2140
|
+
});
|
|
2141
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.updated", rigId: rig.id });
|
|
2142
|
+
return updated;
|
|
2143
|
+
}
|
|
2144
|
+
async function deleteRigForApi(deps, grant, rig) {
|
|
2145
|
+
const workspaceId = grant.workspaceId;
|
|
2146
|
+
const deleted = await deleteRigIfNoActiveSessions(deps.db, workspaceId, rig.id);
|
|
2147
|
+
if (deleted.activeSessionCount > 0) {
|
|
2148
|
+
throw new HTTPException7(409, {
|
|
2149
|
+
message: `rig is referenced by ${deleted.activeSessionCount} active session(s); it cannot be deleted`
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
if (!deleted.deleted) {
|
|
2153
|
+
throw new HTTPException7(404, { message: "rig not found" });
|
|
2154
|
+
}
|
|
2155
|
+
await recordRigAuditEvent(deps.db, { grant, action: "rig.deleted", rigId: rig.id });
|
|
2156
|
+
}
|
|
2157
|
+
async function proposeRigChangeForApi(deps, grant, rig, request, options = {}) {
|
|
2158
|
+
const workspaceId = grant.workspaceId;
|
|
2159
|
+
if (!rig.activeVersion) {
|
|
2160
|
+
throw new HTTPException7(422, { message: "rig has no active version to base a change on" });
|
|
2161
|
+
}
|
|
2162
|
+
if (request.kind === "definition_edit") {
|
|
2163
|
+
assertUniqueCheckNames(request.payload.checks);
|
|
2164
|
+
await assertVariableSetsExist(
|
|
2165
|
+
deps.db,
|
|
2166
|
+
workspaceId,
|
|
2167
|
+
request.payload.defaultVariableSetIds ?? void 0
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
const change = await createRigChange(deps.db, {
|
|
2171
|
+
accountId: grant.accountId,
|
|
2172
|
+
workspaceId,
|
|
2173
|
+
rigId: rig.id,
|
|
2174
|
+
baseVersionId: rig.activeVersion.id,
|
|
2175
|
+
kind: request.kind,
|
|
2176
|
+
payload: request.payload,
|
|
2177
|
+
proposedBy: options.proposedBy ?? rigActorForGrant(grant)
|
|
2178
|
+
});
|
|
2179
|
+
await recordRigAuditEvent(deps.db, {
|
|
2180
|
+
grant,
|
|
2181
|
+
action: "rig.change.proposed",
|
|
2182
|
+
rigId: rig.id,
|
|
2183
|
+
metadata: { changeId: change.id, kind: change.kind }
|
|
2184
|
+
});
|
|
2185
|
+
return change;
|
|
2186
|
+
}
|
|
2187
|
+
function classifyRigVerificationOutcome(input) {
|
|
2188
|
+
if (input.infraError) {
|
|
2189
|
+
return { status: "failed", action: "retryable_failure" };
|
|
2190
|
+
}
|
|
2191
|
+
if (!input.passed) {
|
|
2192
|
+
return { status: "rejected", action: "reject" };
|
|
2193
|
+
}
|
|
2194
|
+
if (input.kind === "setup_append") {
|
|
2195
|
+
return { status: "merged", action: "auto_promote" };
|
|
2196
|
+
}
|
|
2197
|
+
return { status: "proposed", action: "await_manage_promote" };
|
|
2198
|
+
}
|
|
2199
|
+
function appendRigSetupCommand(baseSetupScript, command) {
|
|
2200
|
+
const base = (baseSetupScript ?? "").trimEnd();
|
|
2201
|
+
return base ? `${base}
|
|
2202
|
+
${command}` : command;
|
|
2203
|
+
}
|
|
2204
|
+
async function promoteChangeWithActiveCas(deps, workspaceId, rigId, changeId, input) {
|
|
2205
|
+
try {
|
|
2206
|
+
return await createRigVersionForChangePromotion(deps.db, workspaceId, rigId, changeId, input);
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
if (error instanceof RigActiveVersionChangedError) {
|
|
2209
|
+
throw new HTTPException7(409, {
|
|
2210
|
+
message: `rig moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
if (error instanceof RigChangeTransitionError) {
|
|
2214
|
+
throw new HTTPException7(409, { message: error.message });
|
|
2215
|
+
}
|
|
2216
|
+
throw error;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
async function promoteSetupAppendChange(deps, grant, rig, change) {
|
|
2220
|
+
if (change.kind !== "setup_append") {
|
|
2221
|
+
throw new HTTPException7(422, {
|
|
2222
|
+
message: "only setup_append changes auto-promote through this path"
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
if (change.status !== "proposed" && change.status !== "verifying") {
|
|
2226
|
+
throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
|
|
2227
|
+
}
|
|
2228
|
+
if (!change.baseVersionId) {
|
|
2229
|
+
throw new HTTPException7(422, { message: "rig change has no base version" });
|
|
2230
|
+
}
|
|
2231
|
+
const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
|
|
2232
|
+
if (!base) {
|
|
2233
|
+
throw new HTTPException7(404, { message: "base rig version not found" });
|
|
2234
|
+
}
|
|
2235
|
+
const payload = change.payload;
|
|
2236
|
+
if (typeof payload.command !== "string" || !payload.command.trim()) {
|
|
2237
|
+
throw new HTTPException7(422, { message: "setup_append change is missing command" });
|
|
2238
|
+
}
|
|
2239
|
+
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
2240
|
+
deps,
|
|
2241
|
+
grant.workspaceId,
|
|
2242
|
+
rig.id,
|
|
2243
|
+
change.id,
|
|
2244
|
+
{
|
|
2245
|
+
expectedActiveVersionId: change.baseVersionId,
|
|
2246
|
+
image: base.image,
|
|
2247
|
+
setupScript: appendRigSetupCommand(base.setupScript, payload.command),
|
|
2248
|
+
checks: base.checks,
|
|
2249
|
+
credentialHooks: base.credentialHooks,
|
|
2250
|
+
defaultVariableSetIds: base.defaultVariableSetIds,
|
|
2251
|
+
changelog: typeof payload.note === "string" && payload.note.trim() ? payload.note : "Verified setup append",
|
|
2252
|
+
createdBy: change.proposedBy ?? rigActorForGrant(grant)
|
|
2253
|
+
}
|
|
2254
|
+
);
|
|
2255
|
+
await recordRigAuditEvent(deps.db, {
|
|
2256
|
+
grant,
|
|
2257
|
+
action: "rig.change.merged",
|
|
2258
|
+
rigId: rig.id,
|
|
2259
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2260
|
+
});
|
|
2261
|
+
await recordRigAuditEvent(deps.db, {
|
|
2262
|
+
grant,
|
|
2263
|
+
action: "rig.version.promoted",
|
|
2264
|
+
rigId: rig.id,
|
|
2265
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2266
|
+
});
|
|
2267
|
+
return { change: updated, version };
|
|
2268
|
+
}
|
|
2269
|
+
async function promoteVerifiedDefinitionEditChangeForApi(deps, grant, rig, change) {
|
|
2270
|
+
if (change.kind !== "definition_edit") {
|
|
2271
|
+
throw new HTTPException7(422, { message: "only definition_edit changes use explicit promote" });
|
|
2272
|
+
}
|
|
2273
|
+
if (change.status !== "proposed") {
|
|
2274
|
+
throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
|
|
2275
|
+
}
|
|
2276
|
+
if (change.verification?.passed !== true) {
|
|
2277
|
+
throw new HTTPException7(422, {
|
|
2278
|
+
message: "definition_edit change must pass verification before promote"
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
if (!change.baseVersionId) {
|
|
2282
|
+
throw new HTTPException7(422, { message: "rig change has no base version" });
|
|
2283
|
+
}
|
|
2284
|
+
const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
|
|
2285
|
+
if (!base) {
|
|
2286
|
+
throw new HTTPException7(404, { message: "base rig version not found" });
|
|
2287
|
+
}
|
|
2288
|
+
const payload = change.payload;
|
|
2289
|
+
const { version, change: updated } = await promoteChangeWithActiveCas(
|
|
2290
|
+
deps,
|
|
2291
|
+
grant.workspaceId,
|
|
2292
|
+
rig.id,
|
|
2293
|
+
change.id,
|
|
2294
|
+
{
|
|
2295
|
+
expectedActiveVersionId: change.baseVersionId,
|
|
2296
|
+
image: payload.image === void 0 ? base.image : payload.image,
|
|
2297
|
+
setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
|
|
2298
|
+
checks: Array.isArray(payload.checks) ? payload.checks : base.checks,
|
|
2299
|
+
credentialHooks: Array.isArray(payload.credentialHooks) ? payload.credentialHooks : base.credentialHooks,
|
|
2300
|
+
defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds) ? payload.defaultVariableSetIds : base.defaultVariableSetIds,
|
|
2301
|
+
changelog: typeof payload.changelog === "string" && payload.changelog.trim() ? payload.changelog : "Verified definition edit",
|
|
2302
|
+
createdBy: rigActorForGrant(grant)
|
|
2303
|
+
}
|
|
2304
|
+
);
|
|
2305
|
+
await recordRigAuditEvent(deps.db, {
|
|
2306
|
+
grant,
|
|
2307
|
+
action: "rig.change.merged",
|
|
2308
|
+
rigId: rig.id,
|
|
2309
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2310
|
+
});
|
|
2311
|
+
await recordRigAuditEvent(deps.db, {
|
|
2312
|
+
grant,
|
|
2313
|
+
action: "rig.version.promoted",
|
|
2314
|
+
rigId: rig.id,
|
|
2315
|
+
metadata: { changeId: change.id, versionId: version.id, version: version.version }
|
|
2316
|
+
});
|
|
2317
|
+
return { change: updated, version };
|
|
2318
|
+
}
|
|
2319
|
+
async function createRigVersionForApi(deps, grant, rig, payload) {
|
|
2320
|
+
if (!rig.activeVersion) {
|
|
2321
|
+
throw new HTTPException7(422, { message: "rig has no active version" });
|
|
2322
|
+
}
|
|
2323
|
+
assertUniqueCheckNames(payload.checks);
|
|
2324
|
+
await assertVariableSetsExist(
|
|
2325
|
+
deps.db,
|
|
2326
|
+
grant.workspaceId,
|
|
2327
|
+
payload.defaultVariableSetIds ?? void 0
|
|
2328
|
+
);
|
|
2329
|
+
const base = rig.activeVersion;
|
|
2330
|
+
const version = await createRigVersion(
|
|
2331
|
+
deps.db,
|
|
2332
|
+
grant.workspaceId,
|
|
2333
|
+
rig.id,
|
|
2334
|
+
{
|
|
2335
|
+
image: payload.image === void 0 ? base.image : payload.image,
|
|
2336
|
+
setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
|
|
2337
|
+
checks: payload.checks ?? base.checks,
|
|
2338
|
+
credentialHooks: payload.credentialHooks ?? base.credentialHooks,
|
|
2339
|
+
defaultVariableSetIds: payload.defaultVariableSetIds ?? base.defaultVariableSetIds,
|
|
2340
|
+
changelog: payload.changelog ?? "Manager-created version",
|
|
2341
|
+
createdBy: rigActorForGrant(grant)
|
|
2342
|
+
},
|
|
2343
|
+
{ activate: true }
|
|
2344
|
+
);
|
|
2345
|
+
await recordRigAuditEvent(deps.db, {
|
|
2346
|
+
grant,
|
|
2347
|
+
action: "rig.version.promoted",
|
|
2348
|
+
rigId: rig.id,
|
|
2349
|
+
metadata: { versionId: version.id, version: version.version, direct: true }
|
|
2350
|
+
});
|
|
2351
|
+
return version;
|
|
2352
|
+
}
|
|
2353
|
+
async function activateRigVersionForApi(deps, grant, rig, versionId) {
|
|
2354
|
+
const workspaceId = grant.workspaceId;
|
|
2355
|
+
const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
|
|
2356
|
+
await recordRigAuditEvent(deps.db, {
|
|
2357
|
+
grant,
|
|
2358
|
+
action: "rig.version.activated",
|
|
2359
|
+
rigId: rig.id,
|
|
2360
|
+
metadata: { versionId: version.id, version: version.version }
|
|
2361
|
+
});
|
|
2362
|
+
return version;
|
|
2363
|
+
}
|
|
2364
|
+
async function listRigVersionsForApi(deps, workspaceId, rigId) {
|
|
2365
|
+
return await listRigVersions(deps.db, workspaceId, rigId);
|
|
2366
|
+
}
|
|
2367
|
+
async function listRigChangesForApi(deps, workspaceId, rigId, limit) {
|
|
2368
|
+
return await listRigChanges(deps.db, workspaceId, rigId, limit);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
1731
2371
|
// src/domain/resources.ts
|
|
1732
2372
|
import {
|
|
1733
2373
|
mergeResourceRefs as mergeContractResourceRefs,
|
|
@@ -1736,26 +2376,27 @@ import {
|
|
|
1736
2376
|
ResourceRefConflictError,
|
|
1737
2377
|
stableJson
|
|
1738
2378
|
} from "@opengeni/contracts";
|
|
1739
|
-
import {
|
|
1740
|
-
|
|
1741
|
-
requireFile
|
|
1742
|
-
} from "@opengeni/db";
|
|
1743
|
-
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
2379
|
+
import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
|
|
2380
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
1744
2381
|
function validateToolRefs(tools, settings) {
|
|
1745
2382
|
const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
1746
2383
|
const out = [];
|
|
1747
2384
|
for (const tool of tools) {
|
|
1748
2385
|
if (tool.kind !== "mcp") {
|
|
1749
|
-
throw new
|
|
2386
|
+
throw new HTTPException8(422, {
|
|
2387
|
+
message: `unsupported tool kind: ${tool.kind}`
|
|
2388
|
+
});
|
|
1750
2389
|
}
|
|
1751
2390
|
const optional = tool.optional === true;
|
|
1752
2391
|
if (!mcpServerIds.has(tool.id)) {
|
|
1753
2392
|
if (optional) {
|
|
1754
2393
|
continue;
|
|
1755
2394
|
}
|
|
1756
|
-
throw new
|
|
2395
|
+
throw new HTTPException8(422, { message: `unknown MCP server id: ${tool.id}` });
|
|
1757
2396
|
}
|
|
1758
|
-
out.push(
|
|
2397
|
+
out.push(
|
|
2398
|
+
optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id }
|
|
2399
|
+
);
|
|
1759
2400
|
}
|
|
1760
2401
|
return mergeToolRefs([], out);
|
|
1761
2402
|
}
|
|
@@ -1783,12 +2424,12 @@ function normalizeResources(resources) {
|
|
|
1783
2424
|
} else {
|
|
1784
2425
|
const url = parseResourceUrl(resource.uri);
|
|
1785
2426
|
if (url.protocol !== "https:" || !url.hostname) {
|
|
1786
|
-
throw new
|
|
2427
|
+
throw new HTTPException8(422, { message: "repository resources must use HTTPS Git URLs" });
|
|
1787
2428
|
}
|
|
1788
2429
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1789
2430
|
const parts = path.split("/").filter(Boolean);
|
|
1790
2431
|
if (parts.length < 2) {
|
|
1791
|
-
throw new
|
|
2432
|
+
throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
|
|
1792
2433
|
}
|
|
1793
2434
|
const repo = parts.join("/");
|
|
1794
2435
|
const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
|
|
@@ -1798,6 +2439,11 @@ function normalizeResources(resources) {
|
|
|
1798
2439
|
ref: resource.ref.trim(),
|
|
1799
2440
|
mountPath,
|
|
1800
2441
|
...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
|
|
2442
|
+
...resource.provider ? { provider: resource.provider } : {},
|
|
2443
|
+
...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
|
|
2444
|
+
...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
|
|
2445
|
+
...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
|
|
2446
|
+
...resource.connectionId ? { connectionId: resource.connectionId } : {},
|
|
1801
2447
|
...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
|
|
1802
2448
|
...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
|
|
1803
2449
|
};
|
|
@@ -1805,7 +2451,9 @@ function normalizeResources(resources) {
|
|
|
1805
2451
|
const key = stableJson(normalized);
|
|
1806
2452
|
const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
|
|
1807
2453
|
if (mounted && mounted !== key) {
|
|
1808
|
-
throw new
|
|
2454
|
+
throw new HTTPException8(422, {
|
|
2455
|
+
message: `duplicate resource mount path: ${normalized.mountPath}`
|
|
2456
|
+
});
|
|
1809
2457
|
}
|
|
1810
2458
|
if (normalized.mountPath) {
|
|
1811
2459
|
mountPaths.set(normalized.mountPath, key);
|
|
@@ -1813,7 +2461,9 @@ function normalizeResources(resources) {
|
|
|
1813
2461
|
const identity = resourceIdentityKey(normalized);
|
|
1814
2462
|
const seenIdentity = identities.get(identity);
|
|
1815
2463
|
if (seenIdentity && seenIdentity !== key) {
|
|
1816
|
-
throw new
|
|
2464
|
+
throw new HTTPException8(422, {
|
|
2465
|
+
message: `duplicate resource with different settings: ${identity}`
|
|
2466
|
+
});
|
|
1817
2467
|
}
|
|
1818
2468
|
identities.set(identity, key);
|
|
1819
2469
|
if (!seenResources.has(key)) {
|
|
@@ -1828,7 +2478,7 @@ function mergeResourceRefs(existing, additions) {
|
|
|
1828
2478
|
return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
|
|
1829
2479
|
} catch (error) {
|
|
1830
2480
|
if (error instanceof ResourceRefConflictError) {
|
|
1831
|
-
throw new
|
|
2481
|
+
throw new HTTPException8(422, { message: error.message });
|
|
1832
2482
|
}
|
|
1833
2483
|
throw error;
|
|
1834
2484
|
}
|
|
@@ -1838,8 +2488,8 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1838
2488
|
if (resource.kind !== "repository") {
|
|
1839
2489
|
return [];
|
|
1840
2490
|
}
|
|
1841
|
-
const installationRaw = resource.githubInstallationId;
|
|
1842
|
-
const repositoryRaw = resource.githubRepositoryId;
|
|
2491
|
+
const installationRaw = resource.githubInstallationId ?? (resource.provider === "github" ? resource.installationId : void 0);
|
|
2492
|
+
const repositoryRaw = resource.githubRepositoryId ?? (resource.provider === "github" ? resource.repositoryId : void 0);
|
|
1843
2493
|
if (installationRaw === null && repositoryRaw === null) {
|
|
1844
2494
|
return [];
|
|
1845
2495
|
}
|
|
@@ -1849,7 +2499,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1849
2499
|
const installationId2 = positiveInteger(installationRaw);
|
|
1850
2500
|
const repositoryId = positiveInteger(repositoryRaw);
|
|
1851
2501
|
if (!installationId2 || !repositoryId) {
|
|
1852
|
-
throw new
|
|
2502
|
+
throw new HTTPException8(422, {
|
|
1853
2503
|
message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
|
|
1854
2504
|
});
|
|
1855
2505
|
}
|
|
@@ -1860,7 +2510,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1860
2510
|
}
|
|
1861
2511
|
const installationId = selected[0].installationId;
|
|
1862
2512
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1863
|
-
throw new
|
|
2513
|
+
throw new HTTPException8(422, {
|
|
1864
2514
|
message: "GitHub App repository resources must belong to one installation"
|
|
1865
2515
|
});
|
|
1866
2516
|
}
|
|
@@ -1871,9 +2521,11 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
|
|
|
1871
2521
|
if (installationId === null) {
|
|
1872
2522
|
return;
|
|
1873
2523
|
}
|
|
1874
|
-
const linkedInstallationIds = new Set(
|
|
2524
|
+
const linkedInstallationIds = new Set(
|
|
2525
|
+
await listGitHubInstallationIdsForWorkspace(db, workspaceId)
|
|
2526
|
+
);
|
|
1875
2527
|
if (!linkedInstallationIds.has(installationId)) {
|
|
1876
|
-
throw new
|
|
2528
|
+
throw new HTTPException8(422, {
|
|
1877
2529
|
message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
|
|
1878
2530
|
});
|
|
1879
2531
|
}
|
|
@@ -1885,22 +2537,24 @@ async function validateFileResources(db, workspaceId, resources) {
|
|
|
1885
2537
|
continue;
|
|
1886
2538
|
}
|
|
1887
2539
|
if (fileIds.has(resource.fileId)) {
|
|
1888
|
-
throw new
|
|
2540
|
+
throw new HTTPException8(422, { message: `duplicate file resource: ${resource.fileId}` });
|
|
1889
2541
|
}
|
|
1890
2542
|
fileIds.add(resource.fileId);
|
|
1891
2543
|
const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
|
|
1892
2544
|
if (!file) {
|
|
1893
|
-
throw new
|
|
2545
|
+
throw new HTTPException8(422, { message: `unknown file resource: ${resource.fileId}` });
|
|
1894
2546
|
}
|
|
1895
2547
|
if (file.status !== "ready") {
|
|
1896
|
-
throw new
|
|
2548
|
+
throw new HTTPException8(422, {
|
|
2549
|
+
message: `file resource ${resource.fileId} is ${file.status}`
|
|
2550
|
+
});
|
|
1897
2551
|
}
|
|
1898
2552
|
}
|
|
1899
2553
|
}
|
|
1900
2554
|
function normalizeMountPath(path) {
|
|
1901
2555
|
const normalized = path.trim().replace(/^\/+|\/+$/g, "");
|
|
1902
2556
|
if (!normalized || normalized.includes("..")) {
|
|
1903
|
-
throw new
|
|
2557
|
+
throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
|
|
1904
2558
|
}
|
|
1905
2559
|
return normalized;
|
|
1906
2560
|
}
|
|
@@ -1908,7 +2562,7 @@ function parseResourceUrl(uri) {
|
|
|
1908
2562
|
try {
|
|
1909
2563
|
return new URL(uri);
|
|
1910
2564
|
} catch {
|
|
1911
|
-
throw new
|
|
2565
|
+
throw new HTTPException8(422, { message: "repository resources must use valid URLs" });
|
|
1912
2566
|
}
|
|
1913
2567
|
}
|
|
1914
2568
|
function positiveInteger(value) {
|
|
@@ -1925,38 +2579,47 @@ function positiveInteger(value) {
|
|
|
1925
2579
|
import {
|
|
1926
2580
|
createScheduledTask,
|
|
1927
2581
|
deleteScheduledTask,
|
|
2582
|
+
getRig as getRig3,
|
|
1928
2583
|
getScheduledTask,
|
|
1929
2584
|
updateScheduledTask
|
|
1930
2585
|
} from "@opengeni/db";
|
|
1931
|
-
import { HTTPException as
|
|
2586
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
1932
2587
|
|
|
1933
2588
|
// src/domain/sessions.ts
|
|
1934
2589
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
1935
|
-
import { configuredAllowedModels } from "@opengeni/config";
|
|
2590
|
+
import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
|
|
1936
2591
|
import {
|
|
1937
2592
|
CreateSessionRequest,
|
|
2593
|
+
evaluateWorkspaceModelPolicy,
|
|
1938
2594
|
reasoningEffortForMetadata
|
|
1939
2595
|
} from "@opengeni/contracts";
|
|
1940
2596
|
import {
|
|
1941
|
-
appendSessionEventsWithLockedSessionUpdate,
|
|
1942
2597
|
createSession,
|
|
1943
|
-
createSessionGoal,
|
|
1944
2598
|
createSessionWithIdempotencyKey,
|
|
1945
|
-
|
|
1946
|
-
|
|
2599
|
+
enqueueSessionMessageAtomically,
|
|
2600
|
+
encryptVariableSetValue as encryptVariableSetValue2,
|
|
1947
2601
|
getAnySessionInGroup,
|
|
1948
2602
|
getEnrollment as getEnrollment2,
|
|
1949
|
-
|
|
2603
|
+
getRig as getRig2,
|
|
2604
|
+
getWorkspaceDefaultRigId,
|
|
2605
|
+
listDistinctVariableSetIdsInGroup,
|
|
2606
|
+
listDistinctRigVersionIdsInGroup,
|
|
1950
2607
|
getSandbox as getSandbox3,
|
|
1951
2608
|
getSession,
|
|
1952
2609
|
getSessionByCreateIdempotencyKey,
|
|
2610
|
+
getSessionLineage,
|
|
1953
2611
|
getSessionTurn,
|
|
2612
|
+
getWorkspaceModelPolicy,
|
|
2613
|
+
initializeSessionStartAtomically,
|
|
1954
2614
|
requireSession as requireSession2,
|
|
1955
|
-
|
|
1956
|
-
|
|
2615
|
+
updateSessionTitle as updateSessionTitleRow,
|
|
2616
|
+
SessionQueueConflictError
|
|
1957
2617
|
} from "@opengeni/db";
|
|
1958
|
-
import {
|
|
1959
|
-
|
|
2618
|
+
import {
|
|
2619
|
+
appendAndPublishEvents,
|
|
2620
|
+
publishDurableSessionEvents
|
|
2621
|
+
} from "@opengeni/events";
|
|
2622
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
1960
2623
|
var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
|
|
1961
2624
|
var maxSessionMcpCredentialHeaders = 16;
|
|
1962
2625
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
@@ -1967,23 +2630,29 @@ function normalizedSessionMcpCredentialHeaders(headers) {
|
|
|
1967
2630
|
}
|
|
1968
2631
|
const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
|
|
1969
2632
|
if (entries.length > maxSessionMcpCredentialHeaders) {
|
|
1970
|
-
throw new
|
|
2633
|
+
throw new HTTPException9(422, {
|
|
2634
|
+
message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
|
|
2635
|
+
});
|
|
1971
2636
|
}
|
|
1972
2637
|
const seen = /* @__PURE__ */ new Set();
|
|
1973
2638
|
for (const [name, value] of entries) {
|
|
1974
2639
|
if (!sessionMcpCredentialHeaderName.test(name)) {
|
|
1975
|
-
throw new
|
|
2640
|
+
throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
|
|
1976
2641
|
}
|
|
1977
2642
|
const lower = name.toLowerCase();
|
|
1978
2643
|
if (seen.has(lower)) {
|
|
1979
|
-
throw new
|
|
2644
|
+
throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
|
|
1980
2645
|
}
|
|
1981
2646
|
seen.add(lower);
|
|
1982
2647
|
if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
|
|
1983
|
-
throw new
|
|
2648
|
+
throw new HTTPException9(422, {
|
|
2649
|
+
message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
|
|
2650
|
+
});
|
|
1984
2651
|
}
|
|
1985
2652
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
1986
|
-
throw new
|
|
2653
|
+
throw new HTTPException9(422, {
|
|
2654
|
+
message: `credential header ${name} contains forbidden control characters`
|
|
2655
|
+
});
|
|
1987
2656
|
}
|
|
1988
2657
|
}
|
|
1989
2658
|
return Object.fromEntries(entries);
|
|
@@ -2014,10 +2683,7 @@ function settingsWithSessionMcpServerConfigs(settings, servers) {
|
|
|
2014
2683
|
const sessionIds = new Set(servers.map((server) => server.id));
|
|
2015
2684
|
return {
|
|
2016
2685
|
...settings,
|
|
2017
|
-
mcpServers: [
|
|
2018
|
-
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
2019
|
-
...servers
|
|
2020
|
-
]
|
|
2686
|
+
mcpServers: [...settings.mcpServers.filter((server) => !sessionIds.has(server.id)), ...servers]
|
|
2021
2687
|
};
|
|
2022
2688
|
}
|
|
2023
2689
|
function settingsWithSessionMcpServerMetadata(settings, servers) {
|
|
@@ -2028,7 +2694,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2028
2694
|
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
2029
2695
|
}
|
|
2030
2696
|
requirePermission(grant, "mcp_servers:attach");
|
|
2031
|
-
const encryptionKey =
|
|
2697
|
+
const encryptionKey = requireVariableSetEncryption(settings);
|
|
2032
2698
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
2033
2699
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2034
2700
|
const runtimeServers = [];
|
|
@@ -2036,15 +2702,18 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2036
2702
|
const metadata = [];
|
|
2037
2703
|
for (const server of servers) {
|
|
2038
2704
|
if (seenIds.has(server.id)) {
|
|
2039
|
-
throw new
|
|
2705
|
+
throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
|
|
2040
2706
|
}
|
|
2041
2707
|
seenIds.add(server.id);
|
|
2042
2708
|
if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
|
|
2043
|
-
throw new
|
|
2709
|
+
throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
|
|
2044
2710
|
}
|
|
2045
2711
|
const headers = normalizedSessionMcpCredentialHeaders(server.headers);
|
|
2046
2712
|
const headersEncrypted = Object.fromEntries(
|
|
2047
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2713
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2714
|
+
name,
|
|
2715
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2716
|
+
])
|
|
2048
2717
|
);
|
|
2049
2718
|
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
2050
2719
|
dbServers.push({
|
|
@@ -2072,22 +2741,27 @@ function validateSessionMcpCredentialUpdates(input) {
|
|
|
2072
2741
|
return [];
|
|
2073
2742
|
}
|
|
2074
2743
|
requirePermission(input.grant, "mcp_servers:attach");
|
|
2075
|
-
const encryptionKey =
|
|
2744
|
+
const encryptionKey = requireVariableSetEncryption(input.settings);
|
|
2076
2745
|
const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
|
|
2077
2746
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2078
2747
|
const encryptedUpdates = input.updates.map((update) => {
|
|
2079
2748
|
if (seenIds.has(update.id)) {
|
|
2080
|
-
throw new
|
|
2749
|
+
throw new HTTPException9(422, {
|
|
2750
|
+
message: `duplicate session MCP credential update id: ${update.id}`
|
|
2751
|
+
});
|
|
2081
2752
|
}
|
|
2082
2753
|
seenIds.add(update.id);
|
|
2083
2754
|
if (!knownIds.has(update.id)) {
|
|
2084
|
-
throw new
|
|
2755
|
+
throw new HTTPException9(422, { message: `unknown session MCP server id: ${update.id}` });
|
|
2085
2756
|
}
|
|
2086
2757
|
const headers = normalizedSessionMcpCredentialHeaders(update.headers);
|
|
2087
2758
|
return {
|
|
2088
2759
|
id: update.id,
|
|
2089
2760
|
headersEncrypted: Object.fromEntries(
|
|
2090
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2761
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2762
|
+
name,
|
|
2763
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2764
|
+
])
|
|
2091
2765
|
)
|
|
2092
2766
|
};
|
|
2093
2767
|
});
|
|
@@ -2100,9 +2774,16 @@ async function createAndStartSession(input) {
|
|
|
2100
2774
|
reasoningEffort: input.reasoningEffort
|
|
2101
2775
|
};
|
|
2102
2776
|
if (input.createIdempotencyKey) {
|
|
2103
|
-
const existing = await getSessionByCreateIdempotencyKey(
|
|
2777
|
+
const existing = await getSessionByCreateIdempotencyKey(
|
|
2778
|
+
input.db,
|
|
2779
|
+
input.workspaceId,
|
|
2780
|
+
input.createIdempotencyKey
|
|
2781
|
+
);
|
|
2104
2782
|
if (existing) {
|
|
2105
|
-
return
|
|
2783
|
+
return await finishStartSession(
|
|
2784
|
+
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2785
|
+
existing
|
|
2786
|
+
);
|
|
2106
2787
|
}
|
|
2107
2788
|
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
2108
2789
|
accountId: input.accountId,
|
|
@@ -2113,7 +2794,9 @@ async function createAndStartSession(input) {
|
|
|
2113
2794
|
metadata: sessionMetadata,
|
|
2114
2795
|
model: input.model,
|
|
2115
2796
|
sandboxBackend: input.sandboxBackend,
|
|
2116
|
-
|
|
2797
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2798
|
+
rigId: input.rigId ?? null,
|
|
2799
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2117
2800
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2118
2801
|
instructions: input.instructions ?? null,
|
|
2119
2802
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2123,7 +2806,10 @@ async function createAndStartSession(input) {
|
|
|
2123
2806
|
mcpServers: input.mcpServers ?? []
|
|
2124
2807
|
});
|
|
2125
2808
|
if (!created) {
|
|
2126
|
-
return
|
|
2809
|
+
return await finishStartSession(
|
|
2810
|
+
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2811
|
+
keyed
|
|
2812
|
+
);
|
|
2127
2813
|
}
|
|
2128
2814
|
return await finishStartSession(input, keyed);
|
|
2129
2815
|
}
|
|
@@ -2136,7 +2822,9 @@ async function createAndStartSession(input) {
|
|
|
2136
2822
|
metadata: sessionMetadata,
|
|
2137
2823
|
model: input.model,
|
|
2138
2824
|
sandboxBackend: input.sandboxBackend,
|
|
2139
|
-
|
|
2825
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2826
|
+
rigId: input.rigId ?? null,
|
|
2827
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2140
2828
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2141
2829
|
instructions: input.instructions ?? null,
|
|
2142
2830
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2147,54 +2835,9 @@ async function createAndStartSession(input) {
|
|
|
2147
2835
|
return await finishStartSession(input, session);
|
|
2148
2836
|
}
|
|
2149
2837
|
async function finishStartSession(input, session) {
|
|
2150
|
-
const goal = input.goal ? await createSessionGoal(input.db, {
|
|
2151
|
-
accountId: session.accountId,
|
|
2152
|
-
workspaceId: session.workspaceId,
|
|
2153
|
-
sessionId: session.id,
|
|
2154
|
-
text: input.goal.text,
|
|
2155
|
-
successCriteria: input.goal.successCriteria ?? null,
|
|
2156
|
-
maxAutoContinuations: input.goal.maxAutoContinuations ?? null,
|
|
2157
|
-
createdBy: "api"
|
|
2158
|
-
}) : null;
|
|
2159
|
-
const initialPayload = {
|
|
2160
|
-
text: input.initialMessage,
|
|
2161
|
-
...input.resources.length ? { resources: input.resources } : {},
|
|
2162
|
-
...input.tools.length ? { tools: input.tools } : {}
|
|
2163
|
-
};
|
|
2164
|
-
const events = await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [
|
|
2165
|
-
{
|
|
2166
|
-
type: "session.created",
|
|
2167
|
-
payload: {
|
|
2168
|
-
status: "queued",
|
|
2169
|
-
...input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {},
|
|
2170
|
-
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
2171
|
-
}
|
|
2172
|
-
},
|
|
2173
|
-
...goal ? [{
|
|
2174
|
-
type: "goal.set",
|
|
2175
|
-
payload: {
|
|
2176
|
-
goalId: goal.id,
|
|
2177
|
-
text: goal.text,
|
|
2178
|
-
...goal.successCriteria ? { successCriteria: goal.successCriteria } : {},
|
|
2179
|
-
version: goal.version,
|
|
2180
|
-
actor: "api",
|
|
2181
|
-
replaced: false
|
|
2182
|
-
}
|
|
2183
|
-
}] : [],
|
|
2184
|
-
{
|
|
2185
|
-
type: "user.message",
|
|
2186
|
-
payload: initialPayload,
|
|
2187
|
-
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2188
|
-
},
|
|
2189
|
-
{ type: "session.status.changed", payload: { status: "queued" } }
|
|
2190
|
-
]);
|
|
2191
|
-
const userEvent = events.find((event) => event.type === "user.message");
|
|
2192
|
-
if (!userEvent) {
|
|
2193
|
-
throw new HTTPException8(500, { message: "failed to append initial user event" });
|
|
2194
|
-
}
|
|
2195
2838
|
if (input.seedTargetSandbox) {
|
|
2196
2839
|
if (session.sandboxBackend === "none") {
|
|
2197
|
-
throw new
|
|
2840
|
+
throw new HTTPException9(422, {
|
|
2198
2841
|
message: "cannot target a machine for a session with no sandbox (backend: none)"
|
|
2199
2842
|
});
|
|
2200
2843
|
}
|
|
@@ -2214,34 +2857,37 @@ async function finishStartSession(input, session) {
|
|
|
2214
2857
|
input.seedTargetSandbox.workingDir ?? null
|
|
2215
2858
|
);
|
|
2216
2859
|
if (!seeded.swapped) {
|
|
2217
|
-
throw new
|
|
2860
|
+
throw new HTTPException9(422, {
|
|
2218
2861
|
message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
|
|
2219
2862
|
});
|
|
2220
2863
|
}
|
|
2221
2864
|
}
|
|
2222
|
-
const
|
|
2223
|
-
await setTemporalWorkflowId(input.db, session.workspaceId, session.id, workflowId);
|
|
2224
|
-
const turn = await enqueueSessionTurn(input.db, {
|
|
2865
|
+
const started = await initializeSessionStartAtomically(input.db, {
|
|
2225
2866
|
accountId: session.accountId,
|
|
2226
2867
|
workspaceId: session.workspaceId,
|
|
2227
2868
|
sessionId: session.id,
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2869
|
+
...input.clientEventId ? { clientEventId: input.clientEventId } : {},
|
|
2870
|
+
reasoningEffortFallback: input.reasoningEffort,
|
|
2871
|
+
createdEventPayload: {
|
|
2872
|
+
...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
|
|
2873
|
+
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
2874
|
+
},
|
|
2875
|
+
goal: input.goal ? {
|
|
2876
|
+
text: input.goal.text,
|
|
2877
|
+
...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
|
|
2878
|
+
...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
|
|
2879
|
+
} : null
|
|
2238
2880
|
});
|
|
2239
|
-
await
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2881
|
+
await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
|
|
2882
|
+
if (started.workflowWakeRevision !== null) {
|
|
2883
|
+
await input.workflowClient.wakeSessionWorkflow({
|
|
2884
|
+
accountId: session.accountId,
|
|
2885
|
+
workspaceId: session.workspaceId,
|
|
2886
|
+
sessionId: session.id,
|
|
2887
|
+
workflowId: started.temporalWorkflowId,
|
|
2888
|
+
wakeRevision: started.workflowWakeRevision
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2245
2891
|
return await requireSession2(input.db, session.workspaceId, session.id);
|
|
2246
2892
|
}
|
|
2247
2893
|
function workflowIdForSession(sessionId) {
|
|
@@ -2257,15 +2903,33 @@ function assertConfiguredModel(settings, model) {
|
|
|
2257
2903
|
if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
2258
2904
|
return;
|
|
2259
2905
|
}
|
|
2260
|
-
throw new
|
|
2906
|
+
throw new HTTPException9(422, { message: `model is not available: ${model}` });
|
|
2907
|
+
}
|
|
2908
|
+
async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
|
|
2909
|
+
if (model === null || model === void 0) {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const policy = await getWorkspaceModelPolicy(db, workspaceId);
|
|
2913
|
+
if (!policy) {
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
const providerId = policyProviderIdForModel(settings, model);
|
|
2917
|
+
const verdict = evaluateWorkspaceModelPolicy(policy, { providerId, modelId: model });
|
|
2918
|
+
if (!verdict.allowed) {
|
|
2919
|
+
throw new HTTPException9(422, {
|
|
2920
|
+
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`
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2261
2923
|
}
|
|
2262
2924
|
async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
|
|
2263
2925
|
const turn = await getSessionTurn(db, workspaceId, turnId);
|
|
2264
2926
|
if (!turn || turn.sessionId !== sessionId) {
|
|
2265
|
-
throw new
|
|
2927
|
+
throw new HTTPException9(404, { message: "session turn not found" });
|
|
2266
2928
|
}
|
|
2267
2929
|
if (turn.status !== "queued") {
|
|
2268
|
-
throw new
|
|
2930
|
+
throw new HTTPException9(409, {
|
|
2931
|
+
message: `turn is ${turn.status}; only queued turns can be changed`
|
|
2932
|
+
});
|
|
2269
2933
|
}
|
|
2270
2934
|
return turn;
|
|
2271
2935
|
}
|
|
@@ -2277,98 +2941,139 @@ async function postUserMessageTurn(input) {
|
|
|
2277
2941
|
const requestedModel = input.model ?? null;
|
|
2278
2942
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
2279
2943
|
assertConfiguredModel(settings, requestedModel);
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2944
|
+
await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
|
|
2945
|
+
let result;
|
|
2946
|
+
try {
|
|
2947
|
+
result = await enqueueSessionMessageAtomically(db, {
|
|
2948
|
+
accountId,
|
|
2949
|
+
workspaceId,
|
|
2950
|
+
sessionId,
|
|
2951
|
+
actor: input.actor ?? accountId,
|
|
2952
|
+
origin: input.origin ?? "human",
|
|
2953
|
+
text: input.text,
|
|
2954
|
+
resources: input.resources,
|
|
2955
|
+
tools: input.tools,
|
|
2956
|
+
model: requestedModel,
|
|
2957
|
+
reasoningEffort: requestedReasoningEffort,
|
|
2958
|
+
clientEventId: input.clientEventId ?? null,
|
|
2959
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
2960
|
+
delivery: input.delivery ?? "queue",
|
|
2961
|
+
...input.expectedControlGeneration !== void 0 ? { expectedControlGeneration: input.expectedControlGeneration } : {},
|
|
2962
|
+
...input.expectedWorkspaceInferenceGeneration !== void 0 ? {
|
|
2963
|
+
expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration
|
|
2964
|
+
} : {},
|
|
2965
|
+
reasoningEffortFallback: settings.openaiReasoningEffort
|
|
2966
|
+
});
|
|
2967
|
+
} catch (error) {
|
|
2968
|
+
if (error instanceof SessionQueueConflictError) {
|
|
2969
|
+
throw new HTTPException9(409, { message: error.message });
|
|
2283
2970
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
throw new HTTPException8(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
|
|
2971
|
+
if (error instanceof Error && error.message.includes("cancelled")) {
|
|
2972
|
+
throw new HTTPException9(409, { message: error.message });
|
|
2287
2973
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const session = await requireSession2(db, workspaceId, sessionId);
|
|
2323
|
-
const turn = await enqueueSessionTurn(db, {
|
|
2324
|
-
accountId,
|
|
2325
|
-
workspaceId,
|
|
2326
|
-
sessionId,
|
|
2327
|
-
triggerEventId: accepted.id,
|
|
2328
|
-
temporalWorkflowId: workflowId,
|
|
2329
|
-
source: "user",
|
|
2330
|
-
prompt: input.text,
|
|
2331
|
-
resources: input.resources,
|
|
2332
|
-
tools: input.tools,
|
|
2333
|
-
model: requestedModel ?? session.model,
|
|
2334
|
-
reasoningEffort: requestedReasoningEffort ?? reasoningEffortForSession(session.metadata, settings.openaiReasoningEffort),
|
|
2335
|
-
sandboxBackend: session.sandboxBackend,
|
|
2336
|
-
metadata: {}
|
|
2337
|
-
});
|
|
2338
|
-
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
|
|
2339
|
-
type: "turn.queued",
|
|
2340
|
-
turnId: turn.id,
|
|
2341
|
-
payload: { turnId: turn.id, triggerEventId: accepted.id, source: turn.source }
|
|
2342
|
-
}]);
|
|
2343
|
-
await workflowClient.wakeSessionWorkflow({ accountId, workspaceId, sessionId, workflowId });
|
|
2344
|
-
return { accepted, turn };
|
|
2974
|
+
if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
|
|
2975
|
+
throw new HTTPException9(422, { message: error.message });
|
|
2976
|
+
}
|
|
2977
|
+
throw error;
|
|
2978
|
+
}
|
|
2979
|
+
await bus.publish(workspaceId, sessionId, result.events);
|
|
2980
|
+
if (result.shouldSignalControl && result.controlEvent) {
|
|
2981
|
+
if (result.workflowWakeRevision === null) {
|
|
2982
|
+
throw new Error("Steer control has no workflow wake revision");
|
|
2983
|
+
}
|
|
2984
|
+
await workflowClient.signalSessionControl({
|
|
2985
|
+
accountId,
|
|
2986
|
+
workspaceId,
|
|
2987
|
+
sessionId,
|
|
2988
|
+
eventId: result.controlEvent.id,
|
|
2989
|
+
workflowId: result.temporalWorkflowId,
|
|
2990
|
+
workflowWakeRevision: result.workflowWakeRevision
|
|
2991
|
+
});
|
|
2992
|
+
} else if (result.shouldWake) {
|
|
2993
|
+
if (result.workflowWakeRevision === null) {
|
|
2994
|
+
throw new Error("Runnable prompt has no workflow wake revision");
|
|
2995
|
+
}
|
|
2996
|
+
await workflowClient.wakeSessionWorkflow({
|
|
2997
|
+
accountId,
|
|
2998
|
+
workspaceId,
|
|
2999
|
+
sessionId,
|
|
3000
|
+
workflowId: result.temporalWorkflowId,
|
|
3001
|
+
wakeRevision: result.workflowWakeRevision
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
return {
|
|
3005
|
+
accepted: result.accepted,
|
|
3006
|
+
turn: result.turn
|
|
3007
|
+
};
|
|
2345
3008
|
}
|
|
2346
3009
|
async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
2347
3010
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2348
3011
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
2349
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
2350
|
-
|
|
2351
|
-
|
|
3012
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3013
|
+
db,
|
|
3014
|
+
workspaceId,
|
|
3015
|
+
settings
|
|
3016
|
+
);
|
|
3017
|
+
const sessionMcpServers = validateSessionMcpServersForCreate(
|
|
3018
|
+
capabilityRuntimeSettings,
|
|
3019
|
+
grant,
|
|
3020
|
+
payload.mcpServers
|
|
3021
|
+
);
|
|
3022
|
+
const runtimeSettings = settingsWithSessionMcpServerConfigs(
|
|
3023
|
+
capabilityRuntimeSettings,
|
|
3024
|
+
sessionMcpServers.runtimeServers
|
|
3025
|
+
);
|
|
2352
3026
|
const resources = normalizeResources(payload.resources);
|
|
2353
3027
|
const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
|
|
2354
3028
|
const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
2355
3029
|
const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
|
|
2356
3030
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
2357
3031
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2358
|
-
throw new
|
|
3032
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2359
3033
|
}
|
|
2360
3034
|
await validateFileResources(db, workspaceId, resources);
|
|
2361
|
-
const
|
|
3035
|
+
const variableSet = payload.variableSetId ? await validateVariableSetAttachment(
|
|
3036
|
+
{ settings, db },
|
|
3037
|
+
grant,
|
|
3038
|
+
workspaceId,
|
|
3039
|
+
payload.variableSetId
|
|
3040
|
+
) : null;
|
|
3041
|
+
const requestedRigId = payload.rigId ?? await getWorkspaceDefaultRigId(db, workspaceId);
|
|
3042
|
+
let frozenRigId = null;
|
|
3043
|
+
let frozenRigVersionId = null;
|
|
3044
|
+
if (requestedRigId) {
|
|
3045
|
+
const rig = await getRig2(db, workspaceId, requestedRigId);
|
|
3046
|
+
if (!rig || !rig.activeVersion) {
|
|
3047
|
+
if (payload.rigId) {
|
|
3048
|
+
throw new HTTPException9(422, {
|
|
3049
|
+
message: rig ? `rig ${payload.rigId} has no active version to bind` : `unknown rigId: ${payload.rigId}`
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
} else {
|
|
3053
|
+
frozenRigId = rig.id;
|
|
3054
|
+
frozenRigVersionId = rig.activeVersion.id;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
2362
3057
|
assertConfiguredModel(settings, payload.model);
|
|
3058
|
+
await assertWorkspaceModelPolicyAllows(
|
|
3059
|
+
db,
|
|
3060
|
+
settings,
|
|
3061
|
+
workspaceId,
|
|
3062
|
+
payload.model ?? settings.openaiModel
|
|
3063
|
+
);
|
|
2363
3064
|
const model = payload.model ?? settings.openaiModel;
|
|
2364
3065
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
2365
3066
|
let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
|
|
2366
3067
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
2367
|
-
throw new
|
|
3068
|
+
throw new HTTPException9(422, {
|
|
3069
|
+
message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
|
|
3070
|
+
});
|
|
2368
3071
|
}
|
|
2369
3072
|
for (const permission of firstPartyMcpPermissions ?? []) {
|
|
2370
3073
|
if (!hasPermission(grant.permissions, permission)) {
|
|
2371
|
-
throw new
|
|
3074
|
+
throw new HTTPException9(403, {
|
|
3075
|
+
message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
|
|
3076
|
+
});
|
|
2372
3077
|
}
|
|
2373
3078
|
}
|
|
2374
3079
|
if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
|
|
@@ -2378,19 +3083,39 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2378
3083
|
const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
|
|
2379
3084
|
let sandboxGroupId = null;
|
|
2380
3085
|
let inheritedBackend;
|
|
2381
|
-
const
|
|
2382
|
-
const
|
|
3086
|
+
const requestedVariableSetId = payload.variableSetId ?? null;
|
|
3087
|
+
const variableSetMatchesGroup = (memberVariableSetId) => memberVariableSetId === requestedVariableSetId;
|
|
3088
|
+
const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
|
|
2383
3089
|
if (sandboxChoice === "shared") {
|
|
2384
3090
|
if (!parentSessionId) {
|
|
2385
|
-
throw new
|
|
3091
|
+
throw new HTTPException9(422, {
|
|
3092
|
+
message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
|
|
3093
|
+
});
|
|
2386
3094
|
}
|
|
2387
3095
|
const parent = await getSession(db, workspaceId, parentSessionId);
|
|
2388
3096
|
if (!parent) {
|
|
2389
|
-
throw new
|
|
3097
|
+
throw new HTTPException9(404, {
|
|
3098
|
+
message: `parent session not found in workspace: ${parentSessionId}`
|
|
3099
|
+
});
|
|
2390
3100
|
}
|
|
2391
|
-
|
|
3101
|
+
const parentBoxed = parent.sandboxBackend !== "none";
|
|
3102
|
+
const variableSetMismatch = parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
|
|
3103
|
+
let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
|
|
3104
|
+
if (parentBoxed && !rigMismatch) {
|
|
3105
|
+
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
3106
|
+
db,
|
|
3107
|
+
workspaceId,
|
|
3108
|
+
parent.sandboxGroupId
|
|
3109
|
+
);
|
|
3110
|
+
rigMismatch = !memberRigVersionIds.every(
|
|
3111
|
+
(memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
if (variableSetMismatch || rigMismatch) {
|
|
2392
3115
|
if (payload.sandbox === "shared") {
|
|
2393
|
-
throw new
|
|
3116
|
+
throw new HTTPException9(422, {
|
|
3117
|
+
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."
|
|
3118
|
+
});
|
|
2394
3119
|
}
|
|
2395
3120
|
} else {
|
|
2396
3121
|
sandboxGroupId = parent.sandboxGroupId;
|
|
@@ -2399,19 +3124,43 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2399
3124
|
} else if (typeof sandboxChoice === "object") {
|
|
2400
3125
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
2401
3126
|
if (!member) {
|
|
2402
|
-
throw new
|
|
3127
|
+
throw new HTTPException9(404, {
|
|
3128
|
+
message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
|
|
3129
|
+
});
|
|
2403
3130
|
}
|
|
2404
3131
|
if (member.sandboxBackend !== "none") {
|
|
2405
|
-
const
|
|
2406
|
-
|
|
2407
|
-
|
|
3132
|
+
const memberVariableSetIds = await listDistinctVariableSetIdsInGroup(
|
|
3133
|
+
db,
|
|
3134
|
+
workspaceId,
|
|
3135
|
+
sandboxChoice.groupId
|
|
3136
|
+
);
|
|
3137
|
+
if (!memberVariableSetIds.every(
|
|
3138
|
+
(memberVariableSetId) => variableSetMatchesGroup(memberVariableSetId)
|
|
3139
|
+
)) {
|
|
3140
|
+
throw new HTTPException9(422, {
|
|
3141
|
+
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.`
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
|
|
3145
|
+
db,
|
|
3146
|
+
workspaceId,
|
|
3147
|
+
sandboxChoice.groupId
|
|
3148
|
+
);
|
|
3149
|
+
if (!memberRigVersionIds.every(
|
|
3150
|
+
(memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
|
|
3151
|
+
)) {
|
|
3152
|
+
throw new HTTPException9(422, {
|
|
3153
|
+
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.`
|
|
3154
|
+
});
|
|
2408
3155
|
}
|
|
2409
3156
|
}
|
|
2410
3157
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2411
3158
|
inheritedBackend = member.sandboxBackend;
|
|
2412
3159
|
}
|
|
2413
3160
|
if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
|
|
2414
|
-
throw new
|
|
3161
|
+
throw new HTTPException9(422, {
|
|
3162
|
+
message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
|
|
3163
|
+
});
|
|
2415
3164
|
}
|
|
2416
3165
|
let machineHomeBackend;
|
|
2417
3166
|
let machineHomeOs;
|
|
@@ -2427,7 +3176,13 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2427
3176
|
}
|
|
2428
3177
|
}
|
|
2429
3178
|
}
|
|
2430
|
-
await requireLimit(deps, {
|
|
3179
|
+
await requireLimit(deps, {
|
|
3180
|
+
accountId: grant.accountId,
|
|
3181
|
+
workspaceId,
|
|
3182
|
+
action: "agent_run:create",
|
|
3183
|
+
quantity: 1,
|
|
3184
|
+
model
|
|
3185
|
+
});
|
|
2431
3186
|
const session = await createAndStartSession({
|
|
2432
3187
|
db,
|
|
2433
3188
|
bus,
|
|
@@ -2452,7 +3207,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2452
3207
|
...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
|
|
2453
3208
|
sandboxGroupId,
|
|
2454
3209
|
metadata: payload.metadata,
|
|
2455
|
-
|
|
3210
|
+
variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
|
|
3211
|
+
// Frozen rig binding (M3): both null for a rig-less session (today's path).
|
|
3212
|
+
rigId: frozenRigId,
|
|
3213
|
+
rigVersionId: frozenRigVersionId,
|
|
2456
3214
|
goal: payload.goal ?? null,
|
|
2457
3215
|
// Per-session persona instructions (already trimmed/validated by the
|
|
2458
3216
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
@@ -2484,9 +3242,16 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2484
3242
|
}
|
|
2485
3243
|
async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
|
|
2486
3244
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2487
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3245
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3246
|
+
db,
|
|
3247
|
+
workspaceId,
|
|
3248
|
+
settings
|
|
3249
|
+
);
|
|
2488
3250
|
const existingSession = await requireSession2(db, workspaceId, sessionId);
|
|
2489
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3251
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3252
|
+
capabilityRuntimeSettings,
|
|
3253
|
+
existingSession.mcpServers
|
|
3254
|
+
);
|
|
2490
3255
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
2491
3256
|
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
2492
3257
|
const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
|
|
@@ -2498,10 +3263,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2498
3263
|
model: input.model ?? existingSession.model
|
|
2499
3264
|
});
|
|
2500
3265
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2501
|
-
throw new
|
|
3266
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2502
3267
|
}
|
|
2503
3268
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
2504
|
-
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3269
|
+
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3270
|
+
...existingSession.resources,
|
|
3271
|
+
...requestedResources
|
|
3272
|
+
]);
|
|
2505
3273
|
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
2506
3274
|
settings,
|
|
2507
3275
|
grant,
|
|
@@ -2522,6 +3290,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2522
3290
|
model: input.model ?? null,
|
|
2523
3291
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
2524
3292
|
mcpCredentialUpdates,
|
|
3293
|
+
delivery: input.delivery ?? "queue",
|
|
3294
|
+
origin: input.origin ?? "human",
|
|
3295
|
+
actor: grant.subjectId,
|
|
3296
|
+
...input.expectedControlGeneration !== void 0 ? { expectedControlGeneration: input.expectedControlGeneration } : {},
|
|
3297
|
+
...input.expectedWorkspaceInferenceGeneration !== void 0 ? {
|
|
3298
|
+
expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration
|
|
3299
|
+
} : {},
|
|
2525
3300
|
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2526
3301
|
});
|
|
2527
3302
|
await recordWorkspaceUsage(deps, {
|
|
@@ -2541,16 +3316,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
|
|
|
2541
3316
|
const { db, bus } = deps;
|
|
2542
3317
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
2543
3318
|
if (result.updated) {
|
|
2544
|
-
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
3319
|
+
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
3320
|
+
{
|
|
3321
|
+
type: "session.title_set",
|
|
3322
|
+
payload: {
|
|
3323
|
+
title: result.title ?? title,
|
|
3324
|
+
source
|
|
3325
|
+
}
|
|
2549
3326
|
}
|
|
2550
|
-
|
|
3327
|
+
]);
|
|
2551
3328
|
}
|
|
2552
3329
|
return result;
|
|
2553
3330
|
}
|
|
3331
|
+
async function readSessionLineage(db, workspaceId, sessionId) {
|
|
3332
|
+
const lineage = await getSessionLineage(db, workspaceId, sessionId);
|
|
3333
|
+
if (!lineage) {
|
|
3334
|
+
throw new HTTPException9(404, { message: "session not found" });
|
|
3335
|
+
}
|
|
3336
|
+
return lineage;
|
|
3337
|
+
}
|
|
2554
3338
|
function withFirstPartyTools(tools, runtimeSettings) {
|
|
2555
3339
|
if (!runtimeSettings.mcpServers.some((server) => server.id === "opengeni")) {
|
|
2556
3340
|
return tools;
|
|
@@ -2558,7 +3342,9 @@ function withFirstPartyTools(tools, runtimeSettings) {
|
|
|
2558
3342
|
return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
|
|
2559
3343
|
}
|
|
2560
3344
|
function hasOwnProperty(value, key) {
|
|
2561
|
-
return Boolean(
|
|
3345
|
+
return Boolean(
|
|
3346
|
+
value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key)
|
|
3347
|
+
);
|
|
2562
3348
|
}
|
|
2563
3349
|
|
|
2564
3350
|
// src/domain/scheduled-tasks.ts
|
|
@@ -2572,18 +3358,24 @@ function scheduledTaskToolsProvided(rawPayload) {
|
|
|
2572
3358
|
);
|
|
2573
3359
|
}
|
|
2574
3360
|
async function createValidatedScheduledTask(input) {
|
|
2575
|
-
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3361
|
+
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3362
|
+
...input,
|
|
3363
|
+
workspaceId: input.grant.workspaceId
|
|
3364
|
+
});
|
|
2576
3365
|
const id = crypto.randomUUID();
|
|
2577
3366
|
validateScheduledTaskSchedule(input.payload.schedule);
|
|
2578
|
-
if (input.payload.
|
|
2579
|
-
await
|
|
3367
|
+
if (input.payload.variableSetId) {
|
|
3368
|
+
await validateVariableSetAttachment(
|
|
2580
3369
|
{ settings: input.settings, db: input.db },
|
|
2581
3370
|
input.grant,
|
|
2582
3371
|
input.grant.workspaceId,
|
|
2583
|
-
input.payload.
|
|
2584
|
-
{ preauthorized: input.
|
|
3372
|
+
input.payload.variableSetId,
|
|
3373
|
+
{ preauthorized: input.variableSetPreauthorized ?? false }
|
|
2585
3374
|
);
|
|
2586
3375
|
}
|
|
3376
|
+
if (input.payload.rigId) {
|
|
3377
|
+
await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
|
|
3378
|
+
}
|
|
2587
3379
|
return await createScheduledTask(input.db, {
|
|
2588
3380
|
id,
|
|
2589
3381
|
accountId: input.grant.accountId,
|
|
@@ -2595,10 +3387,17 @@ async function createValidatedScheduledTask(input) {
|
|
|
2595
3387
|
runMode: input.payload.runMode,
|
|
2596
3388
|
overlapPolicy: input.payload.overlapPolicy,
|
|
2597
3389
|
agentConfig,
|
|
2598
|
-
|
|
3390
|
+
variableSetId: input.payload.variableSetId ?? null,
|
|
3391
|
+
rigId: input.payload.rigId ?? null,
|
|
2599
3392
|
metadata: input.payload.metadata
|
|
2600
3393
|
});
|
|
2601
3394
|
}
|
|
3395
|
+
async function requireScheduledTaskRig(db, workspaceId, rigId) {
|
|
3396
|
+
const rig = await getRig3(db, workspaceId, rigId);
|
|
3397
|
+
if (!rig) {
|
|
3398
|
+
throw new HTTPException10(422, { message: `unknown rigId: ${rigId}` });
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
2602
3401
|
async function validatedScheduledTaskUpdate(input) {
|
|
2603
3402
|
const update = {};
|
|
2604
3403
|
if (input.payload.name !== void 0) {
|
|
@@ -2620,30 +3419,38 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2620
3419
|
if (input.payload.metadata !== void 0) {
|
|
2621
3420
|
update.metadata = input.payload.metadata;
|
|
2622
3421
|
}
|
|
2623
|
-
if (input.payload.
|
|
2624
|
-
const
|
|
2625
|
-
if ((input.existing.
|
|
2626
|
-
throw new
|
|
3422
|
+
if (input.payload.variableSetId !== void 0) {
|
|
3423
|
+
const nextVariableSetId = input.payload.variableSetId;
|
|
3424
|
+
if ((input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
|
|
3425
|
+
throw new HTTPException10(409, {
|
|
3426
|
+
message: "cannot change variableSet of a task with a live reusable session; recreate the task"
|
|
3427
|
+
});
|
|
2627
3428
|
}
|
|
2628
|
-
if (
|
|
2629
|
-
if (input.existing.
|
|
2630
|
-
requirePermission(input.grant, "
|
|
3429
|
+
if (nextVariableSetId === null) {
|
|
3430
|
+
if (input.existing.variableSetId !== null) {
|
|
3431
|
+
requirePermission(input.grant, "variable-sets:use");
|
|
2631
3432
|
}
|
|
2632
|
-
update.
|
|
3433
|
+
update.variableSetId = null;
|
|
2633
3434
|
} else {
|
|
2634
|
-
await
|
|
3435
|
+
await validateVariableSetAttachment(
|
|
2635
3436
|
{ settings: input.settings, db: input.db },
|
|
2636
3437
|
input.grant,
|
|
2637
3438
|
input.existing.workspaceId,
|
|
2638
|
-
|
|
3439
|
+
nextVariableSetId
|
|
2639
3440
|
);
|
|
2640
|
-
update.
|
|
3441
|
+
update.variableSetId = nextVariableSetId;
|
|
2641
3442
|
}
|
|
2642
3443
|
}
|
|
3444
|
+
if (input.payload.rigId !== void 0) {
|
|
3445
|
+
if (input.payload.rigId !== null) {
|
|
3446
|
+
await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
|
|
3447
|
+
}
|
|
3448
|
+
update.rigId = input.payload.rigId;
|
|
3449
|
+
}
|
|
2643
3450
|
if (input.payload.agentConfig !== void 0) {
|
|
2644
|
-
const
|
|
2645
|
-
if (
|
|
2646
|
-
requirePermission(input.grant, "
|
|
3451
|
+
const willHaveVariableSet = input.payload.variableSetId !== void 0 ? input.payload.variableSetId !== null : Boolean(input.existing.variableSetId);
|
|
3452
|
+
if (willHaveVariableSet) {
|
|
3453
|
+
requirePermission(input.grant, "variable-sets:use");
|
|
2647
3454
|
}
|
|
2648
3455
|
update.agentConfig = await validateScheduledTaskAgentConfig({
|
|
2649
3456
|
settings: input.settings,
|
|
@@ -2659,7 +3466,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2659
3466
|
async function requireScheduledTaskForApi(db, workspaceId, taskId) {
|
|
2660
3467
|
const task = await getScheduledTask(db, workspaceId, taskId);
|
|
2661
3468
|
if (!task) {
|
|
2662
|
-
throw new
|
|
3469
|
+
throw new HTTPException10(404, { message: "scheduled task not found" });
|
|
2663
3470
|
}
|
|
2664
3471
|
return task;
|
|
2665
3472
|
}
|
|
@@ -2672,7 +3479,7 @@ async function restoreScheduledTask(db, task) {
|
|
|
2672
3479
|
overlapPolicy: task.overlapPolicy,
|
|
2673
3480
|
agentConfig: task.agentConfig,
|
|
2674
3481
|
reusableSessionId: task.reusableSessionId,
|
|
2675
|
-
|
|
3482
|
+
variableSetId: task.variableSetId,
|
|
2676
3483
|
metadata: task.metadata
|
|
2677
3484
|
});
|
|
2678
3485
|
}
|
|
@@ -2680,7 +3487,9 @@ async function syncCreatedScheduledTask(input) {
|
|
|
2680
3487
|
try {
|
|
2681
3488
|
await input.workflowClient.syncScheduledTask({ task: input.task });
|
|
2682
3489
|
} catch (error) {
|
|
2683
|
-
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
|
|
3490
|
+
await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
|
|
3491
|
+
() => void 0
|
|
3492
|
+
);
|
|
2684
3493
|
throw error;
|
|
2685
3494
|
}
|
|
2686
3495
|
}
|
|
@@ -2711,17 +3520,27 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
|
|
|
2711
3520
|
}
|
|
2712
3521
|
async function validateScheduledTaskAgentConfig(input) {
|
|
2713
3522
|
assertConfiguredModel(input.settings, input.payload.agentConfig.model);
|
|
3523
|
+
await assertWorkspaceModelPolicyAllows(
|
|
3524
|
+
input.db,
|
|
3525
|
+
input.settings,
|
|
3526
|
+
input.workspaceId,
|
|
3527
|
+
input.payload.agentConfig.model
|
|
3528
|
+
);
|
|
2714
3529
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
2715
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3530
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3531
|
+
input.db,
|
|
3532
|
+
input.workspaceId,
|
|
3533
|
+
input.settings
|
|
3534
|
+
);
|
|
2716
3535
|
const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
|
|
2717
3536
|
const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
|
|
2718
3537
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
2719
3538
|
if (!prompt) {
|
|
2720
|
-
throw new
|
|
3539
|
+
throw new HTTPException10(422, { message: "scheduled task prompt is required" });
|
|
2721
3540
|
}
|
|
2722
3541
|
await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
|
|
2723
3542
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
2724
|
-
throw new
|
|
3543
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
2725
3544
|
}
|
|
2726
3545
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
2727
3546
|
return {
|
|
@@ -2736,19 +3555,19 @@ function validateScheduledTaskSchedule(schedule) {
|
|
|
2736
3555
|
return;
|
|
2737
3556
|
}
|
|
2738
3557
|
if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
|
|
2739
|
-
throw new
|
|
3558
|
+
throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
|
|
2740
3559
|
}
|
|
2741
3560
|
}
|
|
2742
3561
|
function trimmedScheduledTaskName(name) {
|
|
2743
3562
|
const trimmed = name.trim();
|
|
2744
3563
|
if (!trimmed) {
|
|
2745
|
-
throw new
|
|
3564
|
+
throw new HTTPException10(422, { message: "scheduled task name is required" });
|
|
2746
3565
|
}
|
|
2747
3566
|
return trimmed;
|
|
2748
3567
|
}
|
|
2749
3568
|
|
|
2750
3569
|
// src/domain/workspace-members.ts
|
|
2751
|
-
import { HTTPException as
|
|
3570
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
2752
3571
|
var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
|
|
2753
3572
|
function memberCanAdminister(member) {
|
|
2754
3573
|
return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
|
|
@@ -2758,55 +3577,71 @@ function isUserMember(member) {
|
|
|
2758
3577
|
}
|
|
2759
3578
|
function resolveMemberSubjectId(userId) {
|
|
2760
3579
|
if (!userId) {
|
|
2761
|
-
throw new
|
|
3580
|
+
throw new HTTPException11(404, { message: "user is not registered" });
|
|
2762
3581
|
}
|
|
2763
3582
|
return `user:${userId}`;
|
|
2764
3583
|
}
|
|
2765
3584
|
function assertWorkspaceMemberRemovable(input) {
|
|
2766
3585
|
const { members, subjectId, callerSubjectId } = input;
|
|
2767
3586
|
if (subjectId === callerSubjectId) {
|
|
2768
|
-
throw new
|
|
3587
|
+
throw new HTTPException11(409, { message: "you cannot remove your own membership" });
|
|
2769
3588
|
}
|
|
2770
3589
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
2771
3590
|
if (!target) {
|
|
2772
|
-
throw new
|
|
3591
|
+
throw new HTTPException11(404, { message: "member not found" });
|
|
2773
3592
|
}
|
|
2774
3593
|
if (memberCanAdminister(target)) {
|
|
2775
|
-
const remainingAdmins = members.filter(
|
|
3594
|
+
const remainingAdmins = members.filter(
|
|
3595
|
+
(member) => member.subjectId !== subjectId && memberCanAdminister(member)
|
|
3596
|
+
);
|
|
2776
3597
|
if (remainingAdmins.length === 0) {
|
|
2777
|
-
throw new
|
|
3598
|
+
throw new HTTPException11(409, {
|
|
3599
|
+
message: "cannot remove the last member who can manage this workspace"
|
|
3600
|
+
});
|
|
2778
3601
|
}
|
|
2779
3602
|
}
|
|
2780
3603
|
}
|
|
2781
3604
|
function assertWorkspaceDeletable(input) {
|
|
2782
3605
|
if (input.workspaceCountForAccount <= 1) {
|
|
2783
|
-
throw new
|
|
3606
|
+
throw new HTTPException11(409, { message: "cannot delete the account's only workspace" });
|
|
2784
3607
|
}
|
|
2785
3608
|
if (input.activeSessionCount > 0) {
|
|
2786
|
-
throw new
|
|
3609
|
+
throw new HTTPException11(409, {
|
|
2787
3610
|
message: "stop the workspace's running sessions before deleting it"
|
|
2788
3611
|
});
|
|
2789
3612
|
}
|
|
2790
3613
|
}
|
|
2791
3614
|
export {
|
|
2792
3615
|
MARKETING_SOCIAL_PACK_ID,
|
|
3616
|
+
MAX_CHECKS_PER_RIG,
|
|
3617
|
+
MAX_CREDENTIAL_HOOKS_PER_RIG,
|
|
3618
|
+
MAX_DEFAULT_VARIABLE_SETS_PER_RIG,
|
|
2793
3619
|
MAX_ENVIRONMENTS_PER_WORKSPACE,
|
|
3620
|
+
MAX_RIGS_PER_WORKSPACE,
|
|
2794
3621
|
MAX_VARIABLES_PER_ENVIRONMENT,
|
|
2795
3622
|
acceptSessionUserMessage,
|
|
3623
|
+
activateRigVersionForApi,
|
|
3624
|
+
appendRigSetupCommand,
|
|
2796
3625
|
applyCapabilityEnablement,
|
|
2797
3626
|
assertAllowedEnvironmentVariableName,
|
|
3627
|
+
assertAllowedVariableSetVariableName,
|
|
2798
3628
|
assertConfiguredModel,
|
|
2799
3629
|
assertPackSandboxImageCompatible,
|
|
2800
3630
|
assertWorkspaceDeletable,
|
|
2801
3631
|
assertWorkspaceMemberRemovable,
|
|
3632
|
+
assertWorkspaceModelPolicyAllows,
|
|
2802
3633
|
buildCapabilityCatalog,
|
|
2803
3634
|
buildFleetContextForSession,
|
|
2804
3635
|
buildMarketingDailyAnalysisAgentConfig,
|
|
2805
3636
|
checkLimit,
|
|
3637
|
+
classifyRigVerificationOutcome,
|
|
2806
3638
|
createAndStartSession,
|
|
2807
3639
|
createCatalogItem,
|
|
3640
|
+
createRigForApi,
|
|
3641
|
+
createRigVersionForApi,
|
|
2808
3642
|
createSessionForRequest,
|
|
2809
3643
|
createValidatedScheduledTask,
|
|
3644
|
+
deleteRigForApi,
|
|
2810
3645
|
disableCapability,
|
|
2811
3646
|
discoverMcpRegistryCapabilities,
|
|
2812
3647
|
enableCapability,
|
|
@@ -2817,6 +3652,8 @@ export {
|
|
|
2817
3652
|
isUserMember,
|
|
2818
3653
|
listCapabilityPacks,
|
|
2819
3654
|
listFleet,
|
|
3655
|
+
listRigChangesForApi,
|
|
3656
|
+
listRigVersionsForApi,
|
|
2820
3657
|
listWorkspaceCapabilityPacks,
|
|
2821
3658
|
manualScheduledTaskTriggerUsageKey,
|
|
2822
3659
|
manualScheduledTaskTriggerWorkflowId,
|
|
@@ -2826,23 +3663,32 @@ export {
|
|
|
2826
3663
|
normalizeResources,
|
|
2827
3664
|
officialMcpRegistryUrl,
|
|
2828
3665
|
postUserMessageTurn,
|
|
3666
|
+
promoteSetupAppendChange,
|
|
3667
|
+
promoteVerifiedDefinitionEditChangeForApi,
|
|
3668
|
+
proposeRigChangeForApi,
|
|
2829
3669
|
provisionSandbox,
|
|
3670
|
+
readSessionLineage,
|
|
2830
3671
|
reasoningEffortForSession,
|
|
2831
|
-
|
|
3672
|
+
recordRigAuditEvent,
|
|
3673
|
+
recordVariableSetAuditEvent,
|
|
2832
3674
|
recordWorkspaceUsage,
|
|
2833
3675
|
relayConfigFromSettings,
|
|
2834
3676
|
relayDialBaseFromSettings,
|
|
2835
3677
|
requireAccessContext,
|
|
2836
3678
|
requireAccessGrant,
|
|
2837
3679
|
requireEnvironmentEncryption,
|
|
2838
|
-
requireEnvironmentForApi,
|
|
2839
3680
|
requireLimit,
|
|
2840
3681
|
requirePermission,
|
|
2841
3682
|
requireQueuedTurnForApi,
|
|
3683
|
+
requireRigChangeForApi,
|
|
3684
|
+
requireRigForApi,
|
|
2842
3685
|
requireScheduledTaskForApi,
|
|
3686
|
+
requireVariableSetEncryption,
|
|
3687
|
+
requireVariableSetForApi,
|
|
2843
3688
|
resolveCapabilityPack,
|
|
2844
3689
|
resolveMemberSubjectId,
|
|
2845
3690
|
restoreScheduledTask,
|
|
3691
|
+
rigActorForGrant,
|
|
2846
3692
|
routingEnabled,
|
|
2847
3693
|
runOnSandbox,
|
|
2848
3694
|
scheduledTaskTemporalScheduleId,
|
|
@@ -2855,13 +3701,14 @@ export {
|
|
|
2855
3701
|
swapActiveSandbox,
|
|
2856
3702
|
syncCreatedScheduledTask,
|
|
2857
3703
|
syncUpdatedScheduledTask,
|
|
3704
|
+
updateRigForApi,
|
|
2858
3705
|
updateSessionTitle,
|
|
2859
|
-
validateEnvironmentAttachment,
|
|
2860
3706
|
validateFileResources,
|
|
2861
3707
|
validateGitHubRepositorySelection,
|
|
2862
3708
|
validateGitHubRepositorySelectionShape,
|
|
2863
3709
|
validateMcpCapabilityConnection,
|
|
2864
3710
|
validateToolRefs,
|
|
3711
|
+
validateVariableSetAttachment,
|
|
2865
3712
|
validatedScheduledTaskUpdate,
|
|
2866
3713
|
withDefaultEnabledCapabilityMcpTools,
|
|
2867
3714
|
workflowIdForSession,
|