@opengeni/core 0.4.6 → 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 +1318 -475
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
- 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 +380 -181
- package/src/domain/environments.ts +58 -38
- package/src/domain/packs.ts +49 -16
- package/src/domain/resources.ts +71 -28
- package/src/domain/scheduled-tasks.ts +107 -41
- package/src/domain/sessions.ts +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({
|
|
@@ -1245,7 +1469,10 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
|
|
|
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,8 +1480,14 @@ 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);
|
|
@@ -1264,18 +1497,32 @@ async function probeStreamableHttpMcpServer(input) {
|
|
|
1264
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();
|
|
1267
|
-
if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
|
|
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
|
+
)) {
|
|
1268
1503
|
return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
|
|
1269
1504
|
}
|
|
1270
1505
|
return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
|
|
1271
1506
|
}
|
|
1272
1507
|
async function disableCapability(input) {
|
|
1273
|
-
const item = await requireCatalogItem(
|
|
1508
|
+
const item = await requireCatalogItem(
|
|
1509
|
+
input.db,
|
|
1510
|
+
input.workspaceId,
|
|
1511
|
+
input.settings,
|
|
1512
|
+
input.capabilityId
|
|
1513
|
+
);
|
|
1274
1514
|
if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
|
|
1275
|
-
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
|
+
});
|
|
1276
1518
|
}
|
|
1277
1519
|
if (item.kind === "pack") {
|
|
1278
|
-
await updatePackInstallationStatus(
|
|
1520
|
+
await updatePackInstallationStatus(
|
|
1521
|
+
input.db,
|
|
1522
|
+
input.workspaceId,
|
|
1523
|
+
packIdFromCapabilityId(item.id),
|
|
1524
|
+
"disabled"
|
|
1525
|
+
).catch(() => void 0);
|
|
1279
1526
|
if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
|
|
1280
1527
|
await enableCapabilityInstallation(input.db, {
|
|
1281
1528
|
accountId: input.accountId,
|
|
@@ -1306,16 +1553,18 @@ function settingsWithMcpCapabilityServers(settings, enabled) {
|
|
|
1306
1553
|
if (headers === "unavailable" && !server.connectionRef) {
|
|
1307
1554
|
return [];
|
|
1308
1555
|
}
|
|
1309
|
-
return [
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
+
];
|
|
1319
1568
|
});
|
|
1320
1569
|
return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
|
|
1321
1570
|
}
|
|
@@ -1369,7 +1618,10 @@ async function discoverMcpRegistryCapabilities(input) {
|
|
|
1369
1618
|
async function fetchMcpRegistryPage(url, options = {}) {
|
|
1370
1619
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1371
1620
|
const controller = new AbortController();
|
|
1372
|
-
const timeout = setTimeout(
|
|
1621
|
+
const timeout = setTimeout(
|
|
1622
|
+
() => controller.abort(),
|
|
1623
|
+
options.timeoutMs ?? mcpRegistryFetchTimeoutMs
|
|
1624
|
+
);
|
|
1373
1625
|
try {
|
|
1374
1626
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
1375
1627
|
if (!response.ok) {
|
|
@@ -1426,28 +1678,30 @@ function packCatalogItem(pack, source) {
|
|
|
1426
1678
|
});
|
|
1427
1679
|
}
|
|
1428
1680
|
function configuredMcpCatalogItems(settings) {
|
|
1429
|
-
return settings.mcpServers.map(
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
+
);
|
|
1451
1705
|
}
|
|
1452
1706
|
function platformApiCatalogItems() {
|
|
1453
1707
|
return [
|
|
@@ -1483,46 +1737,56 @@ function platformApiCatalogItems() {
|
|
|
1483
1737
|
tags: ["api", "schedules", "agents"],
|
|
1484
1738
|
endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks"
|
|
1485
1739
|
}
|
|
1486
|
-
].map(
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
+
);
|
|
1502
1758
|
}
|
|
1503
1759
|
async function discoverBundledSkills() {
|
|
1504
|
-
const skillsDir = new URL(
|
|
1760
|
+
const skillsDir = new URL(
|
|
1761
|
+
"../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
|
|
1762
|
+
import.meta.url
|
|
1763
|
+
);
|
|
1505
1764
|
try {
|
|
1506
1765
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1507
|
-
const skills = await Promise.all(
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
+
);
|
|
1526
1790
|
return skills;
|
|
1527
1791
|
} catch {
|
|
1528
1792
|
return [];
|
|
@@ -1599,7 +1863,11 @@ function firstPartyMcpDescription(id) {
|
|
|
1599
1863
|
return null;
|
|
1600
1864
|
}
|
|
1601
1865
|
function generatedCapabilityId(payload) {
|
|
1602
|
-
const source = [
|
|
1866
|
+
const source = [
|
|
1867
|
+
payload.kind,
|
|
1868
|
+
payload.name,
|
|
1869
|
+
payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""
|
|
1870
|
+
].join(":");
|
|
1603
1871
|
return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
|
|
1604
1872
|
}
|
|
1605
1873
|
function publicRegistryCapabilityId(name, version, endpointUrl) {
|
|
@@ -1637,7 +1905,9 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1637
1905
|
if (official?.isLatest === false) {
|
|
1638
1906
|
return null;
|
|
1639
1907
|
}
|
|
1640
|
-
const remote = server.remotes?.find(
|
|
1908
|
+
const remote = server.remotes?.find(
|
|
1909
|
+
(candidate) => candidate.type === "streamable-http" && candidate.url
|
|
1910
|
+
);
|
|
1641
1911
|
const endpointUrl = validUrl(remote?.url);
|
|
1642
1912
|
if (!remote || !endpointUrl) {
|
|
1643
1913
|
return null;
|
|
@@ -1654,7 +1924,12 @@ function mcpRegistryEntryToCatalogItem(entry) {
|
|
|
1654
1924
|
name: server.title || server.name,
|
|
1655
1925
|
description: server.description ?? null,
|
|
1656
1926
|
category: "public-mcp",
|
|
1657
|
-
tags: [
|
|
1927
|
+
tags: [
|
|
1928
|
+
"mcp",
|
|
1929
|
+
"public",
|
|
1930
|
+
"registry",
|
|
1931
|
+
...requiredHeaders.length ? ["requires-credentials"] : []
|
|
1932
|
+
],
|
|
1658
1933
|
homepageUrl,
|
|
1659
1934
|
endpointUrl,
|
|
1660
1935
|
installUrl: homepageUrl,
|
|
@@ -1732,6 +2007,367 @@ function storedConnectionRef(config) {
|
|
|
1732
2007
|
return !!ref && typeof ref === "object" && !Array.isArray(ref) && typeof ref.providerDomain === "string";
|
|
1733
2008
|
}
|
|
1734
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
|
+
|
|
1735
2371
|
// src/domain/resources.ts
|
|
1736
2372
|
import {
|
|
1737
2373
|
mergeResourceRefs as mergeContractResourceRefs,
|
|
@@ -1740,26 +2376,27 @@ import {
|
|
|
1740
2376
|
ResourceRefConflictError,
|
|
1741
2377
|
stableJson
|
|
1742
2378
|
} from "@opengeni/contracts";
|
|
1743
|
-
import {
|
|
1744
|
-
|
|
1745
|
-
requireFile
|
|
1746
|
-
} from "@opengeni/db";
|
|
1747
|
-
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
2379
|
+
import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
|
|
2380
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
1748
2381
|
function validateToolRefs(tools, settings) {
|
|
1749
2382
|
const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
1750
2383
|
const out = [];
|
|
1751
2384
|
for (const tool of tools) {
|
|
1752
2385
|
if (tool.kind !== "mcp") {
|
|
1753
|
-
throw new
|
|
2386
|
+
throw new HTTPException8(422, {
|
|
2387
|
+
message: `unsupported tool kind: ${tool.kind}`
|
|
2388
|
+
});
|
|
1754
2389
|
}
|
|
1755
2390
|
const optional = tool.optional === true;
|
|
1756
2391
|
if (!mcpServerIds.has(tool.id)) {
|
|
1757
2392
|
if (optional) {
|
|
1758
2393
|
continue;
|
|
1759
2394
|
}
|
|
1760
|
-
throw new
|
|
2395
|
+
throw new HTTPException8(422, { message: `unknown MCP server id: ${tool.id}` });
|
|
1761
2396
|
}
|
|
1762
|
-
out.push(
|
|
2397
|
+
out.push(
|
|
2398
|
+
optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id }
|
|
2399
|
+
);
|
|
1763
2400
|
}
|
|
1764
2401
|
return mergeToolRefs([], out);
|
|
1765
2402
|
}
|
|
@@ -1787,12 +2424,12 @@ function normalizeResources(resources) {
|
|
|
1787
2424
|
} else {
|
|
1788
2425
|
const url = parseResourceUrl(resource.uri);
|
|
1789
2426
|
if (url.protocol !== "https:" || !url.hostname) {
|
|
1790
|
-
throw new
|
|
2427
|
+
throw new HTTPException8(422, { message: "repository resources must use HTTPS Git URLs" });
|
|
1791
2428
|
}
|
|
1792
2429
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1793
2430
|
const parts = path.split("/").filter(Boolean);
|
|
1794
2431
|
if (parts.length < 2) {
|
|
1795
|
-
throw new
|
|
2432
|
+
throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
|
|
1796
2433
|
}
|
|
1797
2434
|
const repo = parts.join("/");
|
|
1798
2435
|
const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
|
|
@@ -1802,6 +2439,11 @@ function normalizeResources(resources) {
|
|
|
1802
2439
|
ref: resource.ref.trim(),
|
|
1803
2440
|
mountPath,
|
|
1804
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 } : {},
|
|
1805
2447
|
...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
|
|
1806
2448
|
...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
|
|
1807
2449
|
};
|
|
@@ -1809,7 +2451,9 @@ function normalizeResources(resources) {
|
|
|
1809
2451
|
const key = stableJson(normalized);
|
|
1810
2452
|
const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
|
|
1811
2453
|
if (mounted && mounted !== key) {
|
|
1812
|
-
throw new
|
|
2454
|
+
throw new HTTPException8(422, {
|
|
2455
|
+
message: `duplicate resource mount path: ${normalized.mountPath}`
|
|
2456
|
+
});
|
|
1813
2457
|
}
|
|
1814
2458
|
if (normalized.mountPath) {
|
|
1815
2459
|
mountPaths.set(normalized.mountPath, key);
|
|
@@ -1817,7 +2461,9 @@ function normalizeResources(resources) {
|
|
|
1817
2461
|
const identity = resourceIdentityKey(normalized);
|
|
1818
2462
|
const seenIdentity = identities.get(identity);
|
|
1819
2463
|
if (seenIdentity && seenIdentity !== key) {
|
|
1820
|
-
throw new
|
|
2464
|
+
throw new HTTPException8(422, {
|
|
2465
|
+
message: `duplicate resource with different settings: ${identity}`
|
|
2466
|
+
});
|
|
1821
2467
|
}
|
|
1822
2468
|
identities.set(identity, key);
|
|
1823
2469
|
if (!seenResources.has(key)) {
|
|
@@ -1832,7 +2478,7 @@ function mergeResourceRefs(existing, additions) {
|
|
|
1832
2478
|
return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
|
|
1833
2479
|
} catch (error) {
|
|
1834
2480
|
if (error instanceof ResourceRefConflictError) {
|
|
1835
|
-
throw new
|
|
2481
|
+
throw new HTTPException8(422, { message: error.message });
|
|
1836
2482
|
}
|
|
1837
2483
|
throw error;
|
|
1838
2484
|
}
|
|
@@ -1842,8 +2488,8 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1842
2488
|
if (resource.kind !== "repository") {
|
|
1843
2489
|
return [];
|
|
1844
2490
|
}
|
|
1845
|
-
const installationRaw = resource.githubInstallationId;
|
|
1846
|
-
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);
|
|
1847
2493
|
if (installationRaw === null && repositoryRaw === null) {
|
|
1848
2494
|
return [];
|
|
1849
2495
|
}
|
|
@@ -1853,7 +2499,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1853
2499
|
const installationId2 = positiveInteger(installationRaw);
|
|
1854
2500
|
const repositoryId = positiveInteger(repositoryRaw);
|
|
1855
2501
|
if (!installationId2 || !repositoryId) {
|
|
1856
|
-
throw new
|
|
2502
|
+
throw new HTTPException8(422, {
|
|
1857
2503
|
message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
|
|
1858
2504
|
});
|
|
1859
2505
|
}
|
|
@@ -1864,7 +2510,7 @@ function validateGitHubRepositorySelectionShape(resources) {
|
|
|
1864
2510
|
}
|
|
1865
2511
|
const installationId = selected[0].installationId;
|
|
1866
2512
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1867
|
-
throw new
|
|
2513
|
+
throw new HTTPException8(422, {
|
|
1868
2514
|
message: "GitHub App repository resources must belong to one installation"
|
|
1869
2515
|
});
|
|
1870
2516
|
}
|
|
@@ -1875,9 +2521,11 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
|
|
|
1875
2521
|
if (installationId === null) {
|
|
1876
2522
|
return;
|
|
1877
2523
|
}
|
|
1878
|
-
const linkedInstallationIds = new Set(
|
|
2524
|
+
const linkedInstallationIds = new Set(
|
|
2525
|
+
await listGitHubInstallationIdsForWorkspace(db, workspaceId)
|
|
2526
|
+
);
|
|
1879
2527
|
if (!linkedInstallationIds.has(installationId)) {
|
|
1880
|
-
throw new
|
|
2528
|
+
throw new HTTPException8(422, {
|
|
1881
2529
|
message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
|
|
1882
2530
|
});
|
|
1883
2531
|
}
|
|
@@ -1889,22 +2537,24 @@ async function validateFileResources(db, workspaceId, resources) {
|
|
|
1889
2537
|
continue;
|
|
1890
2538
|
}
|
|
1891
2539
|
if (fileIds.has(resource.fileId)) {
|
|
1892
|
-
throw new
|
|
2540
|
+
throw new HTTPException8(422, { message: `duplicate file resource: ${resource.fileId}` });
|
|
1893
2541
|
}
|
|
1894
2542
|
fileIds.add(resource.fileId);
|
|
1895
2543
|
const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
|
|
1896
2544
|
if (!file) {
|
|
1897
|
-
throw new
|
|
2545
|
+
throw new HTTPException8(422, { message: `unknown file resource: ${resource.fileId}` });
|
|
1898
2546
|
}
|
|
1899
2547
|
if (file.status !== "ready") {
|
|
1900
|
-
throw new
|
|
2548
|
+
throw new HTTPException8(422, {
|
|
2549
|
+
message: `file resource ${resource.fileId} is ${file.status}`
|
|
2550
|
+
});
|
|
1901
2551
|
}
|
|
1902
2552
|
}
|
|
1903
2553
|
}
|
|
1904
2554
|
function normalizeMountPath(path) {
|
|
1905
2555
|
const normalized = path.trim().replace(/^\/+|\/+$/g, "");
|
|
1906
2556
|
if (!normalized || normalized.includes("..")) {
|
|
1907
|
-
throw new
|
|
2557
|
+
throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
|
|
1908
2558
|
}
|
|
1909
2559
|
return normalized;
|
|
1910
2560
|
}
|
|
@@ -1912,7 +2562,7 @@ function parseResourceUrl(uri) {
|
|
|
1912
2562
|
try {
|
|
1913
2563
|
return new URL(uri);
|
|
1914
2564
|
} catch {
|
|
1915
|
-
throw new
|
|
2565
|
+
throw new HTTPException8(422, { message: "repository resources must use valid URLs" });
|
|
1916
2566
|
}
|
|
1917
2567
|
}
|
|
1918
2568
|
function positiveInteger(value) {
|
|
@@ -1929,38 +2579,47 @@ function positiveInteger(value) {
|
|
|
1929
2579
|
import {
|
|
1930
2580
|
createScheduledTask,
|
|
1931
2581
|
deleteScheduledTask,
|
|
2582
|
+
getRig as getRig3,
|
|
1932
2583
|
getScheduledTask,
|
|
1933
2584
|
updateScheduledTask
|
|
1934
2585
|
} from "@opengeni/db";
|
|
1935
|
-
import { HTTPException as
|
|
2586
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
1936
2587
|
|
|
1937
2588
|
// src/domain/sessions.ts
|
|
1938
2589
|
import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
|
|
1939
|
-
import { configuredAllowedModels } from "@opengeni/config";
|
|
2590
|
+
import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
|
|
1940
2591
|
import {
|
|
1941
2592
|
CreateSessionRequest,
|
|
2593
|
+
evaluateWorkspaceModelPolicy,
|
|
1942
2594
|
reasoningEffortForMetadata
|
|
1943
2595
|
} from "@opengeni/contracts";
|
|
1944
2596
|
import {
|
|
1945
|
-
appendSessionEventsWithLockedSessionUpdate,
|
|
1946
2597
|
createSession,
|
|
1947
|
-
createSessionGoal,
|
|
1948
2598
|
createSessionWithIdempotencyKey,
|
|
1949
|
-
|
|
1950
|
-
|
|
2599
|
+
enqueueSessionMessageAtomically,
|
|
2600
|
+
encryptVariableSetValue as encryptVariableSetValue2,
|
|
1951
2601
|
getAnySessionInGroup,
|
|
1952
2602
|
getEnrollment as getEnrollment2,
|
|
1953
|
-
|
|
2603
|
+
getRig as getRig2,
|
|
2604
|
+
getWorkspaceDefaultRigId,
|
|
2605
|
+
listDistinctVariableSetIdsInGroup,
|
|
2606
|
+
listDistinctRigVersionIdsInGroup,
|
|
1954
2607
|
getSandbox as getSandbox3,
|
|
1955
2608
|
getSession,
|
|
1956
2609
|
getSessionByCreateIdempotencyKey,
|
|
2610
|
+
getSessionLineage,
|
|
1957
2611
|
getSessionTurn,
|
|
2612
|
+
getWorkspaceModelPolicy,
|
|
2613
|
+
initializeSessionStartAtomically,
|
|
1958
2614
|
requireSession as requireSession2,
|
|
1959
|
-
|
|
1960
|
-
|
|
2615
|
+
updateSessionTitle as updateSessionTitleRow,
|
|
2616
|
+
SessionQueueConflictError
|
|
1961
2617
|
} from "@opengeni/db";
|
|
1962
|
-
import {
|
|
1963
|
-
|
|
2618
|
+
import {
|
|
2619
|
+
appendAndPublishEvents,
|
|
2620
|
+
publishDurableSessionEvents
|
|
2621
|
+
} from "@opengeni/events";
|
|
2622
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
1964
2623
|
var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
|
|
1965
2624
|
var maxSessionMcpCredentialHeaders = 16;
|
|
1966
2625
|
var maxSessionMcpCredentialHeaderValueLength = 4096;
|
|
@@ -1971,23 +2630,29 @@ function normalizedSessionMcpCredentialHeaders(headers) {
|
|
|
1971
2630
|
}
|
|
1972
2631
|
const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
|
|
1973
2632
|
if (entries.length > maxSessionMcpCredentialHeaders) {
|
|
1974
|
-
throw new
|
|
2633
|
+
throw new HTTPException9(422, {
|
|
2634
|
+
message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
|
|
2635
|
+
});
|
|
1975
2636
|
}
|
|
1976
2637
|
const seen = /* @__PURE__ */ new Set();
|
|
1977
2638
|
for (const [name, value] of entries) {
|
|
1978
2639
|
if (!sessionMcpCredentialHeaderName.test(name)) {
|
|
1979
|
-
throw new
|
|
2640
|
+
throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
|
|
1980
2641
|
}
|
|
1981
2642
|
const lower = name.toLowerCase();
|
|
1982
2643
|
if (seen.has(lower)) {
|
|
1983
|
-
throw new
|
|
2644
|
+
throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
|
|
1984
2645
|
}
|
|
1985
2646
|
seen.add(lower);
|
|
1986
2647
|
if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
|
|
1987
|
-
throw new
|
|
2648
|
+
throw new HTTPException9(422, {
|
|
2649
|
+
message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
|
|
2650
|
+
});
|
|
1988
2651
|
}
|
|
1989
2652
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
1990
|
-
throw new
|
|
2653
|
+
throw new HTTPException9(422, {
|
|
2654
|
+
message: `credential header ${name} contains forbidden control characters`
|
|
2655
|
+
});
|
|
1991
2656
|
}
|
|
1992
2657
|
}
|
|
1993
2658
|
return Object.fromEntries(entries);
|
|
@@ -2018,10 +2683,7 @@ function settingsWithSessionMcpServerConfigs(settings, servers) {
|
|
|
2018
2683
|
const sessionIds = new Set(servers.map((server) => server.id));
|
|
2019
2684
|
return {
|
|
2020
2685
|
...settings,
|
|
2021
|
-
mcpServers: [
|
|
2022
|
-
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
2023
|
-
...servers
|
|
2024
|
-
]
|
|
2686
|
+
mcpServers: [...settings.mcpServers.filter((server) => !sessionIds.has(server.id)), ...servers]
|
|
2025
2687
|
};
|
|
2026
2688
|
}
|
|
2027
2689
|
function settingsWithSessionMcpServerMetadata(settings, servers) {
|
|
@@ -2032,7 +2694,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2032
2694
|
return { runtimeServers: [], dbServers: [], metadata: [] };
|
|
2033
2695
|
}
|
|
2034
2696
|
requirePermission(grant, "mcp_servers:attach");
|
|
2035
|
-
const encryptionKey =
|
|
2697
|
+
const encryptionKey = requireVariableSetEncryption(settings);
|
|
2036
2698
|
const existingIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
2037
2699
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2038
2700
|
const runtimeServers = [];
|
|
@@ -2040,15 +2702,18 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
|
|
|
2040
2702
|
const metadata = [];
|
|
2041
2703
|
for (const server of servers) {
|
|
2042
2704
|
if (seenIds.has(server.id)) {
|
|
2043
|
-
throw new
|
|
2705
|
+
throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
|
|
2044
2706
|
}
|
|
2045
2707
|
seenIds.add(server.id);
|
|
2046
2708
|
if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
|
|
2047
|
-
throw new
|
|
2709
|
+
throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
|
|
2048
2710
|
}
|
|
2049
2711
|
const headers = normalizedSessionMcpCredentialHeaders(server.headers);
|
|
2050
2712
|
const headersEncrypted = Object.fromEntries(
|
|
2051
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2713
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2714
|
+
name,
|
|
2715
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2716
|
+
])
|
|
2052
2717
|
);
|
|
2053
2718
|
runtimeServers.push(mcpServerConfigFromInput(server));
|
|
2054
2719
|
dbServers.push({
|
|
@@ -2076,22 +2741,27 @@ function validateSessionMcpCredentialUpdates(input) {
|
|
|
2076
2741
|
return [];
|
|
2077
2742
|
}
|
|
2078
2743
|
requirePermission(input.grant, "mcp_servers:attach");
|
|
2079
|
-
const encryptionKey =
|
|
2744
|
+
const encryptionKey = requireVariableSetEncryption(input.settings);
|
|
2080
2745
|
const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
|
|
2081
2746
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2082
2747
|
const encryptedUpdates = input.updates.map((update) => {
|
|
2083
2748
|
if (seenIds.has(update.id)) {
|
|
2084
|
-
throw new
|
|
2749
|
+
throw new HTTPException9(422, {
|
|
2750
|
+
message: `duplicate session MCP credential update id: ${update.id}`
|
|
2751
|
+
});
|
|
2085
2752
|
}
|
|
2086
2753
|
seenIds.add(update.id);
|
|
2087
2754
|
if (!knownIds.has(update.id)) {
|
|
2088
|
-
throw new
|
|
2755
|
+
throw new HTTPException9(422, { message: `unknown session MCP server id: ${update.id}` });
|
|
2089
2756
|
}
|
|
2090
2757
|
const headers = normalizedSessionMcpCredentialHeaders(update.headers);
|
|
2091
2758
|
return {
|
|
2092
2759
|
id: update.id,
|
|
2093
2760
|
headersEncrypted: Object.fromEntries(
|
|
2094
|
-
Object.entries(headers).map(([name, value]) => [
|
|
2761
|
+
Object.entries(headers).map(([name, value]) => [
|
|
2762
|
+
name,
|
|
2763
|
+
encryptVariableSetValue2(encryptionKey, value)
|
|
2764
|
+
])
|
|
2095
2765
|
)
|
|
2096
2766
|
};
|
|
2097
2767
|
});
|
|
@@ -2104,9 +2774,16 @@ async function createAndStartSession(input) {
|
|
|
2104
2774
|
reasoningEffort: input.reasoningEffort
|
|
2105
2775
|
};
|
|
2106
2776
|
if (input.createIdempotencyKey) {
|
|
2107
|
-
const existing = await getSessionByCreateIdempotencyKey(
|
|
2777
|
+
const existing = await getSessionByCreateIdempotencyKey(
|
|
2778
|
+
input.db,
|
|
2779
|
+
input.workspaceId,
|
|
2780
|
+
input.createIdempotencyKey
|
|
2781
|
+
);
|
|
2108
2782
|
if (existing) {
|
|
2109
|
-
return
|
|
2783
|
+
return await finishStartSession(
|
|
2784
|
+
existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2785
|
+
existing
|
|
2786
|
+
);
|
|
2110
2787
|
}
|
|
2111
2788
|
const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
|
|
2112
2789
|
accountId: input.accountId,
|
|
@@ -2117,7 +2794,9 @@ async function createAndStartSession(input) {
|
|
|
2117
2794
|
metadata: sessionMetadata,
|
|
2118
2795
|
model: input.model,
|
|
2119
2796
|
sandboxBackend: input.sandboxBackend,
|
|
2120
|
-
|
|
2797
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2798
|
+
rigId: input.rigId ?? null,
|
|
2799
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2121
2800
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2122
2801
|
instructions: input.instructions ?? null,
|
|
2123
2802
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2127,7 +2806,10 @@ async function createAndStartSession(input) {
|
|
|
2127
2806
|
mcpServers: input.mcpServers ?? []
|
|
2128
2807
|
});
|
|
2129
2808
|
if (!created) {
|
|
2130
|
-
return
|
|
2809
|
+
return await finishStartSession(
|
|
2810
|
+
keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
|
|
2811
|
+
keyed
|
|
2812
|
+
);
|
|
2131
2813
|
}
|
|
2132
2814
|
return await finishStartSession(input, keyed);
|
|
2133
2815
|
}
|
|
@@ -2140,7 +2822,9 @@ async function createAndStartSession(input) {
|
|
|
2140
2822
|
metadata: sessionMetadata,
|
|
2141
2823
|
model: input.model,
|
|
2142
2824
|
sandboxBackend: input.sandboxBackend,
|
|
2143
|
-
|
|
2825
|
+
variableSetId: input.variableSet?.id ?? null,
|
|
2826
|
+
rigId: input.rigId ?? null,
|
|
2827
|
+
rigVersionId: input.rigVersionId ?? null,
|
|
2144
2828
|
firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
|
|
2145
2829
|
instructions: input.instructions ?? null,
|
|
2146
2830
|
parentSessionId: input.parentSessionId ?? null,
|
|
@@ -2151,54 +2835,9 @@ async function createAndStartSession(input) {
|
|
|
2151
2835
|
return await finishStartSession(input, session);
|
|
2152
2836
|
}
|
|
2153
2837
|
async function finishStartSession(input, session) {
|
|
2154
|
-
const goal = input.goal ? await createSessionGoal(input.db, {
|
|
2155
|
-
accountId: session.accountId,
|
|
2156
|
-
workspaceId: session.workspaceId,
|
|
2157
|
-
sessionId: session.id,
|
|
2158
|
-
text: input.goal.text,
|
|
2159
|
-
successCriteria: input.goal.successCriteria ?? null,
|
|
2160
|
-
maxAutoContinuations: input.goal.maxAutoContinuations ?? null,
|
|
2161
|
-
createdBy: "api"
|
|
2162
|
-
}) : null;
|
|
2163
|
-
const initialPayload = {
|
|
2164
|
-
text: input.initialMessage,
|
|
2165
|
-
...input.resources.length ? { resources: input.resources } : {},
|
|
2166
|
-
...input.tools.length ? { tools: input.tools } : {}
|
|
2167
|
-
};
|
|
2168
|
-
const events = await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [
|
|
2169
|
-
{
|
|
2170
|
-
type: "session.created",
|
|
2171
|
-
payload: {
|
|
2172
|
-
status: "queued",
|
|
2173
|
-
...input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {},
|
|
2174
|
-
...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
|
|
2175
|
-
}
|
|
2176
|
-
},
|
|
2177
|
-
...goal ? [{
|
|
2178
|
-
type: "goal.set",
|
|
2179
|
-
payload: {
|
|
2180
|
-
goalId: goal.id,
|
|
2181
|
-
text: goal.text,
|
|
2182
|
-
...goal.successCriteria ? { successCriteria: goal.successCriteria } : {},
|
|
2183
|
-
version: goal.version,
|
|
2184
|
-
actor: "api",
|
|
2185
|
-
replaced: false
|
|
2186
|
-
}
|
|
2187
|
-
}] : [],
|
|
2188
|
-
{
|
|
2189
|
-
type: "user.message",
|
|
2190
|
-
payload: initialPayload,
|
|
2191
|
-
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2192
|
-
},
|
|
2193
|
-
{ type: "session.status.changed", payload: { status: "queued" } }
|
|
2194
|
-
]);
|
|
2195
|
-
const userEvent = events.find((event) => event.type === "user.message");
|
|
2196
|
-
if (!userEvent) {
|
|
2197
|
-
throw new HTTPException8(500, { message: "failed to append initial user event" });
|
|
2198
|
-
}
|
|
2199
2838
|
if (input.seedTargetSandbox) {
|
|
2200
2839
|
if (session.sandboxBackend === "none") {
|
|
2201
|
-
throw new
|
|
2840
|
+
throw new HTTPException9(422, {
|
|
2202
2841
|
message: "cannot target a machine for a session with no sandbox (backend: none)"
|
|
2203
2842
|
});
|
|
2204
2843
|
}
|
|
@@ -2218,34 +2857,37 @@ async function finishStartSession(input, session) {
|
|
|
2218
2857
|
input.seedTargetSandbox.workingDir ?? null
|
|
2219
2858
|
);
|
|
2220
2859
|
if (!seeded.swapped) {
|
|
2221
|
-
throw new
|
|
2860
|
+
throw new HTTPException9(422, {
|
|
2222
2861
|
message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
|
|
2223
2862
|
});
|
|
2224
2863
|
}
|
|
2225
2864
|
}
|
|
2226
|
-
const
|
|
2227
|
-
await setTemporalWorkflowId(input.db, session.workspaceId, session.id, workflowId);
|
|
2228
|
-
const turn = await enqueueSessionTurn(input.db, {
|
|
2865
|
+
const started = await initializeSessionStartAtomically(input.db, {
|
|
2229
2866
|
accountId: session.accountId,
|
|
2230
2867
|
workspaceId: session.workspaceId,
|
|
2231
2868
|
sessionId: session.id,
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
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
|
|
2242
2880
|
});
|
|
2243
|
-
await
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
+
}
|
|
2249
2891
|
return await requireSession2(input.db, session.workspaceId, session.id);
|
|
2250
2892
|
}
|
|
2251
2893
|
function workflowIdForSession(sessionId) {
|
|
@@ -2261,15 +2903,33 @@ function assertConfiguredModel(settings, model) {
|
|
|
2261
2903
|
if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
|
|
2262
2904
|
return;
|
|
2263
2905
|
}
|
|
2264
|
-
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
|
+
}
|
|
2265
2923
|
}
|
|
2266
2924
|
async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
|
|
2267
2925
|
const turn = await getSessionTurn(db, workspaceId, turnId);
|
|
2268
2926
|
if (!turn || turn.sessionId !== sessionId) {
|
|
2269
|
-
throw new
|
|
2927
|
+
throw new HTTPException9(404, { message: "session turn not found" });
|
|
2270
2928
|
}
|
|
2271
2929
|
if (turn.status !== "queued") {
|
|
2272
|
-
throw new
|
|
2930
|
+
throw new HTTPException9(409, {
|
|
2931
|
+
message: `turn is ${turn.status}; only queued turns can be changed`
|
|
2932
|
+
});
|
|
2273
2933
|
}
|
|
2274
2934
|
return turn;
|
|
2275
2935
|
}
|
|
@@ -2281,98 +2941,139 @@ async function postUserMessageTurn(input) {
|
|
|
2281
2941
|
const requestedModel = input.model ?? null;
|
|
2282
2942
|
const requestedReasoningEffort = input.reasoningEffort ?? null;
|
|
2283
2943
|
assertConfiguredModel(settings, requestedModel);
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
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 });
|
|
2287
2970
|
}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
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 });
|
|
2291
2973
|
}
|
|
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
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
const session = await requireSession2(db, workspaceId, sessionId);
|
|
2327
|
-
const turn = await enqueueSessionTurn(db, {
|
|
2328
|
-
accountId,
|
|
2329
|
-
workspaceId,
|
|
2330
|
-
sessionId,
|
|
2331
|
-
triggerEventId: accepted.id,
|
|
2332
|
-
temporalWorkflowId: workflowId,
|
|
2333
|
-
source: "user",
|
|
2334
|
-
prompt: input.text,
|
|
2335
|
-
resources: input.resources,
|
|
2336
|
-
tools: input.tools,
|
|
2337
|
-
model: requestedModel ?? session.model,
|
|
2338
|
-
reasoningEffort: requestedReasoningEffort ?? reasoningEffortForSession(session.metadata, settings.openaiReasoningEffort),
|
|
2339
|
-
sandboxBackend: session.sandboxBackend,
|
|
2340
|
-
metadata: {}
|
|
2341
|
-
});
|
|
2342
|
-
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
|
|
2343
|
-
type: "turn.queued",
|
|
2344
|
-
turnId: turn.id,
|
|
2345
|
-
payload: { turnId: turn.id, triggerEventId: accepted.id, source: turn.source }
|
|
2346
|
-
}]);
|
|
2347
|
-
await workflowClient.wakeSessionWorkflow({ accountId, workspaceId, sessionId, workflowId });
|
|
2348
|
-
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
|
+
};
|
|
2349
3008
|
}
|
|
2350
3009
|
async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
2351
3010
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2352
3011
|
const payload = CreateSessionRequest.parse(rawPayload);
|
|
2353
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
2354
|
-
|
|
2355
|
-
|
|
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
|
+
);
|
|
2356
3026
|
const resources = normalizeResources(payload.resources);
|
|
2357
3027
|
const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
|
|
2358
3028
|
const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
|
|
2359
3029
|
const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
|
|
2360
3030
|
await validateGitHubRepositorySelection(db, workspaceId, resources);
|
|
2361
3031
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2362
|
-
throw new
|
|
3032
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2363
3033
|
}
|
|
2364
3034
|
await validateFileResources(db, workspaceId, resources);
|
|
2365
|
-
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
|
+
}
|
|
2366
3057
|
assertConfiguredModel(settings, payload.model);
|
|
3058
|
+
await assertWorkspaceModelPolicyAllows(
|
|
3059
|
+
db,
|
|
3060
|
+
settings,
|
|
3061
|
+
workspaceId,
|
|
3062
|
+
payload.model ?? settings.openaiModel
|
|
3063
|
+
);
|
|
2367
3064
|
const model = payload.model ?? settings.openaiModel;
|
|
2368
3065
|
const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
|
|
2369
3066
|
let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
|
|
2370
3067
|
if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
|
|
2371
|
-
throw new
|
|
3068
|
+
throw new HTTPException9(422, {
|
|
3069
|
+
message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
|
|
3070
|
+
});
|
|
2372
3071
|
}
|
|
2373
3072
|
for (const permission of firstPartyMcpPermissions ?? []) {
|
|
2374
3073
|
if (!hasPermission(grant.permissions, permission)) {
|
|
2375
|
-
throw new
|
|
3074
|
+
throw new HTTPException9(403, {
|
|
3075
|
+
message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
|
|
3076
|
+
});
|
|
2376
3077
|
}
|
|
2377
3078
|
}
|
|
2378
3079
|
if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
|
|
@@ -2382,19 +3083,39 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2382
3083
|
const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
|
|
2383
3084
|
let sandboxGroupId = null;
|
|
2384
3085
|
let inheritedBackend;
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
3086
|
+
const requestedVariableSetId = payload.variableSetId ?? null;
|
|
3087
|
+
const variableSetMatchesGroup = (memberVariableSetId) => memberVariableSetId === requestedVariableSetId;
|
|
3088
|
+
const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
|
|
2387
3089
|
if (sandboxChoice === "shared") {
|
|
2388
3090
|
if (!parentSessionId) {
|
|
2389
|
-
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
|
+
});
|
|
2390
3094
|
}
|
|
2391
3095
|
const parent = await getSession(db, workspaceId, parentSessionId);
|
|
2392
3096
|
if (!parent) {
|
|
2393
|
-
throw new
|
|
3097
|
+
throw new HTTPException9(404, {
|
|
3098
|
+
message: `parent session not found in workspace: ${parentSessionId}`
|
|
3099
|
+
});
|
|
2394
3100
|
}
|
|
2395
|
-
|
|
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) {
|
|
2396
3115
|
if (payload.sandbox === "shared") {
|
|
2397
|
-
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
|
+
});
|
|
2398
3119
|
}
|
|
2399
3120
|
} else {
|
|
2400
3121
|
sandboxGroupId = parent.sandboxGroupId;
|
|
@@ -2403,19 +3124,43 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2403
3124
|
} else if (typeof sandboxChoice === "object") {
|
|
2404
3125
|
const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
|
|
2405
3126
|
if (!member) {
|
|
2406
|
-
throw new
|
|
3127
|
+
throw new HTTPException9(404, {
|
|
3128
|
+
message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
|
|
3129
|
+
});
|
|
2407
3130
|
}
|
|
2408
3131
|
if (member.sandboxBackend !== "none") {
|
|
2409
|
-
const
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
+
});
|
|
2412
3155
|
}
|
|
2413
3156
|
}
|
|
2414
3157
|
sandboxGroupId = sandboxChoice.groupId;
|
|
2415
3158
|
inheritedBackend = member.sandboxBackend;
|
|
2416
3159
|
}
|
|
2417
3160
|
if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
|
|
2418
|
-
throw new
|
|
3161
|
+
throw new HTTPException9(422, {
|
|
3162
|
+
message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
|
|
3163
|
+
});
|
|
2419
3164
|
}
|
|
2420
3165
|
let machineHomeBackend;
|
|
2421
3166
|
let machineHomeOs;
|
|
@@ -2431,7 +3176,13 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2431
3176
|
}
|
|
2432
3177
|
}
|
|
2433
3178
|
}
|
|
2434
|
-
await requireLimit(deps, {
|
|
3179
|
+
await requireLimit(deps, {
|
|
3180
|
+
accountId: grant.accountId,
|
|
3181
|
+
workspaceId,
|
|
3182
|
+
action: "agent_run:create",
|
|
3183
|
+
quantity: 1,
|
|
3184
|
+
model
|
|
3185
|
+
});
|
|
2435
3186
|
const session = await createAndStartSession({
|
|
2436
3187
|
db,
|
|
2437
3188
|
bus,
|
|
@@ -2456,7 +3207,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2456
3207
|
...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
|
|
2457
3208
|
sandboxGroupId,
|
|
2458
3209
|
metadata: payload.metadata,
|
|
2459
|
-
|
|
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,
|
|
2460
3214
|
goal: payload.goal ?? null,
|
|
2461
3215
|
// Per-session persona instructions (already trimmed/validated by the
|
|
2462
3216
|
// contracts schema). Persisted on the row; composed system-level at turn
|
|
@@ -2488,9 +3242,16 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
|
|
|
2488
3242
|
}
|
|
2489
3243
|
async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
|
|
2490
3244
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
2491
|
-
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3245
|
+
const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3246
|
+
db,
|
|
3247
|
+
workspaceId,
|
|
3248
|
+
settings
|
|
3249
|
+
);
|
|
2492
3250
|
const existingSession = await requireSession2(db, workspaceId, sessionId);
|
|
2493
|
-
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3251
|
+
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
3252
|
+
capabilityRuntimeSettings,
|
|
3253
|
+
existingSession.mcpServers
|
|
3254
|
+
);
|
|
2494
3255
|
const requestedResources = normalizeResources(input.resources ?? []);
|
|
2495
3256
|
const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
|
|
2496
3257
|
const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
|
|
@@ -2502,10 +3263,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2502
3263
|
model: input.model ?? existingSession.model
|
|
2503
3264
|
});
|
|
2504
3265
|
if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
2505
|
-
throw new
|
|
3266
|
+
throw new HTTPException9(503, { message: "object storage is not configured" });
|
|
2506
3267
|
}
|
|
2507
3268
|
await validateFileResources(db, workspaceId, requestedResources);
|
|
2508
|
-
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3269
|
+
await validateGitHubRepositorySelection(db, workspaceId, [
|
|
3270
|
+
...existingSession.resources,
|
|
3271
|
+
...requestedResources
|
|
3272
|
+
]);
|
|
2509
3273
|
const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
|
|
2510
3274
|
settings,
|
|
2511
3275
|
grant,
|
|
@@ -2526,6 +3290,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
|
|
|
2526
3290
|
model: input.model ?? null,
|
|
2527
3291
|
reasoningEffort: input.reasoningEffort ?? null,
|
|
2528
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
|
+
} : {},
|
|
2529
3300
|
...input.clientEventId ? { clientEventId: input.clientEventId } : {}
|
|
2530
3301
|
});
|
|
2531
3302
|
await recordWorkspaceUsage(deps, {
|
|
@@ -2545,16 +3316,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
|
|
|
2545
3316
|
const { db, bus } = deps;
|
|
2546
3317
|
const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
|
|
2547
3318
|
if (result.updated) {
|
|
2548
|
-
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
3319
|
+
await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
|
|
3320
|
+
{
|
|
3321
|
+
type: "session.title_set",
|
|
3322
|
+
payload: {
|
|
3323
|
+
title: result.title ?? title,
|
|
3324
|
+
source
|
|
3325
|
+
}
|
|
2553
3326
|
}
|
|
2554
|
-
|
|
3327
|
+
]);
|
|
2555
3328
|
}
|
|
2556
3329
|
return result;
|
|
2557
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
|
+
}
|
|
2558
3338
|
function withFirstPartyTools(tools, runtimeSettings) {
|
|
2559
3339
|
if (!runtimeSettings.mcpServers.some((server) => server.id === "opengeni")) {
|
|
2560
3340
|
return tools;
|
|
@@ -2562,7 +3342,9 @@ function withFirstPartyTools(tools, runtimeSettings) {
|
|
|
2562
3342
|
return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
|
|
2563
3343
|
}
|
|
2564
3344
|
function hasOwnProperty(value, key) {
|
|
2565
|
-
return Boolean(
|
|
3345
|
+
return Boolean(
|
|
3346
|
+
value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key)
|
|
3347
|
+
);
|
|
2566
3348
|
}
|
|
2567
3349
|
|
|
2568
3350
|
// src/domain/scheduled-tasks.ts
|
|
@@ -2576,18 +3358,24 @@ function scheduledTaskToolsProvided(rawPayload) {
|
|
|
2576
3358
|
);
|
|
2577
3359
|
}
|
|
2578
3360
|
async function createValidatedScheduledTask(input) {
|
|
2579
|
-
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3361
|
+
const agentConfig = await validateScheduledTaskAgentConfig({
|
|
3362
|
+
...input,
|
|
3363
|
+
workspaceId: input.grant.workspaceId
|
|
3364
|
+
});
|
|
2580
3365
|
const id = crypto.randomUUID();
|
|
2581
3366
|
validateScheduledTaskSchedule(input.payload.schedule);
|
|
2582
|
-
if (input.payload.
|
|
2583
|
-
await
|
|
3367
|
+
if (input.payload.variableSetId) {
|
|
3368
|
+
await validateVariableSetAttachment(
|
|
2584
3369
|
{ settings: input.settings, db: input.db },
|
|
2585
3370
|
input.grant,
|
|
2586
3371
|
input.grant.workspaceId,
|
|
2587
|
-
input.payload.
|
|
2588
|
-
{ preauthorized: input.
|
|
3372
|
+
input.payload.variableSetId,
|
|
3373
|
+
{ preauthorized: input.variableSetPreauthorized ?? false }
|
|
2589
3374
|
);
|
|
2590
3375
|
}
|
|
3376
|
+
if (input.payload.rigId) {
|
|
3377
|
+
await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
|
|
3378
|
+
}
|
|
2591
3379
|
return await createScheduledTask(input.db, {
|
|
2592
3380
|
id,
|
|
2593
3381
|
accountId: input.grant.accountId,
|
|
@@ -2599,10 +3387,17 @@ async function createValidatedScheduledTask(input) {
|
|
|
2599
3387
|
runMode: input.payload.runMode,
|
|
2600
3388
|
overlapPolicy: input.payload.overlapPolicy,
|
|
2601
3389
|
agentConfig,
|
|
2602
|
-
|
|
3390
|
+
variableSetId: input.payload.variableSetId ?? null,
|
|
3391
|
+
rigId: input.payload.rigId ?? null,
|
|
2603
3392
|
metadata: input.payload.metadata
|
|
2604
3393
|
});
|
|
2605
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
|
+
}
|
|
2606
3401
|
async function validatedScheduledTaskUpdate(input) {
|
|
2607
3402
|
const update = {};
|
|
2608
3403
|
if (input.payload.name !== void 0) {
|
|
@@ -2624,30 +3419,38 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2624
3419
|
if (input.payload.metadata !== void 0) {
|
|
2625
3420
|
update.metadata = input.payload.metadata;
|
|
2626
3421
|
}
|
|
2627
|
-
if (input.payload.
|
|
2628
|
-
const
|
|
2629
|
-
if ((input.existing.
|
|
2630
|
-
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
|
+
});
|
|
2631
3428
|
}
|
|
2632
|
-
if (
|
|
2633
|
-
if (input.existing.
|
|
2634
|
-
requirePermission(input.grant, "
|
|
3429
|
+
if (nextVariableSetId === null) {
|
|
3430
|
+
if (input.existing.variableSetId !== null) {
|
|
3431
|
+
requirePermission(input.grant, "variable-sets:use");
|
|
2635
3432
|
}
|
|
2636
|
-
update.
|
|
3433
|
+
update.variableSetId = null;
|
|
2637
3434
|
} else {
|
|
2638
|
-
await
|
|
3435
|
+
await validateVariableSetAttachment(
|
|
2639
3436
|
{ settings: input.settings, db: input.db },
|
|
2640
3437
|
input.grant,
|
|
2641
3438
|
input.existing.workspaceId,
|
|
2642
|
-
|
|
3439
|
+
nextVariableSetId
|
|
2643
3440
|
);
|
|
2644
|
-
update.
|
|
3441
|
+
update.variableSetId = nextVariableSetId;
|
|
2645
3442
|
}
|
|
2646
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
|
+
}
|
|
2647
3450
|
if (input.payload.agentConfig !== void 0) {
|
|
2648
|
-
const
|
|
2649
|
-
if (
|
|
2650
|
-
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");
|
|
2651
3454
|
}
|
|
2652
3455
|
update.agentConfig = await validateScheduledTaskAgentConfig({
|
|
2653
3456
|
settings: input.settings,
|
|
@@ -2663,7 +3466,7 @@ async function validatedScheduledTaskUpdate(input) {
|
|
|
2663
3466
|
async function requireScheduledTaskForApi(db, workspaceId, taskId) {
|
|
2664
3467
|
const task = await getScheduledTask(db, workspaceId, taskId);
|
|
2665
3468
|
if (!task) {
|
|
2666
|
-
throw new
|
|
3469
|
+
throw new HTTPException10(404, { message: "scheduled task not found" });
|
|
2667
3470
|
}
|
|
2668
3471
|
return task;
|
|
2669
3472
|
}
|
|
@@ -2676,7 +3479,7 @@ async function restoreScheduledTask(db, task) {
|
|
|
2676
3479
|
overlapPolicy: task.overlapPolicy,
|
|
2677
3480
|
agentConfig: task.agentConfig,
|
|
2678
3481
|
reusableSessionId: task.reusableSessionId,
|
|
2679
|
-
|
|
3482
|
+
variableSetId: task.variableSetId,
|
|
2680
3483
|
metadata: task.metadata
|
|
2681
3484
|
});
|
|
2682
3485
|
}
|
|
@@ -2684,7 +3487,9 @@ async function syncCreatedScheduledTask(input) {
|
|
|
2684
3487
|
try {
|
|
2685
3488
|
await input.workflowClient.syncScheduledTask({ task: input.task });
|
|
2686
3489
|
} catch (error) {
|
|
2687
|
-
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
|
+
);
|
|
2688
3493
|
throw error;
|
|
2689
3494
|
}
|
|
2690
3495
|
}
|
|
@@ -2715,17 +3520,27 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
|
|
|
2715
3520
|
}
|
|
2716
3521
|
async function validateScheduledTaskAgentConfig(input) {
|
|
2717
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
|
+
);
|
|
2718
3529
|
const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
|
|
2719
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3530
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
3531
|
+
input.db,
|
|
3532
|
+
input.workspaceId,
|
|
3533
|
+
input.settings
|
|
3534
|
+
);
|
|
2720
3535
|
const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
|
|
2721
3536
|
const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
|
|
2722
3537
|
const prompt = input.payload.agentConfig.prompt.trim();
|
|
2723
3538
|
if (!prompt) {
|
|
2724
|
-
throw new
|
|
3539
|
+
throw new HTTPException10(422, { message: "scheduled task prompt is required" });
|
|
2725
3540
|
}
|
|
2726
3541
|
await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
|
|
2727
3542
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
2728
|
-
throw new
|
|
3543
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
2729
3544
|
}
|
|
2730
3545
|
await validateFileResources(input.db, input.workspaceId, resources);
|
|
2731
3546
|
return {
|
|
@@ -2740,19 +3555,19 @@ function validateScheduledTaskSchedule(schedule) {
|
|
|
2740
3555
|
return;
|
|
2741
3556
|
}
|
|
2742
3557
|
if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
|
|
2743
|
-
throw new
|
|
3558
|
+
throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
|
|
2744
3559
|
}
|
|
2745
3560
|
}
|
|
2746
3561
|
function trimmedScheduledTaskName(name) {
|
|
2747
3562
|
const trimmed = name.trim();
|
|
2748
3563
|
if (!trimmed) {
|
|
2749
|
-
throw new
|
|
3564
|
+
throw new HTTPException10(422, { message: "scheduled task name is required" });
|
|
2750
3565
|
}
|
|
2751
3566
|
return trimmed;
|
|
2752
3567
|
}
|
|
2753
3568
|
|
|
2754
3569
|
// src/domain/workspace-members.ts
|
|
2755
|
-
import { HTTPException as
|
|
3570
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
2756
3571
|
var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
|
|
2757
3572
|
function memberCanAdminister(member) {
|
|
2758
3573
|
return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
|
|
@@ -2762,55 +3577,71 @@ function isUserMember(member) {
|
|
|
2762
3577
|
}
|
|
2763
3578
|
function resolveMemberSubjectId(userId) {
|
|
2764
3579
|
if (!userId) {
|
|
2765
|
-
throw new
|
|
3580
|
+
throw new HTTPException11(404, { message: "user is not registered" });
|
|
2766
3581
|
}
|
|
2767
3582
|
return `user:${userId}`;
|
|
2768
3583
|
}
|
|
2769
3584
|
function assertWorkspaceMemberRemovable(input) {
|
|
2770
3585
|
const { members, subjectId, callerSubjectId } = input;
|
|
2771
3586
|
if (subjectId === callerSubjectId) {
|
|
2772
|
-
throw new
|
|
3587
|
+
throw new HTTPException11(409, { message: "you cannot remove your own membership" });
|
|
2773
3588
|
}
|
|
2774
3589
|
const target = members.find((member) => member.subjectId === subjectId);
|
|
2775
3590
|
if (!target) {
|
|
2776
|
-
throw new
|
|
3591
|
+
throw new HTTPException11(404, { message: "member not found" });
|
|
2777
3592
|
}
|
|
2778
3593
|
if (memberCanAdminister(target)) {
|
|
2779
|
-
const remainingAdmins = members.filter(
|
|
3594
|
+
const remainingAdmins = members.filter(
|
|
3595
|
+
(member) => member.subjectId !== subjectId && memberCanAdminister(member)
|
|
3596
|
+
);
|
|
2780
3597
|
if (remainingAdmins.length === 0) {
|
|
2781
|
-
throw new
|
|
3598
|
+
throw new HTTPException11(409, {
|
|
3599
|
+
message: "cannot remove the last member who can manage this workspace"
|
|
3600
|
+
});
|
|
2782
3601
|
}
|
|
2783
3602
|
}
|
|
2784
3603
|
}
|
|
2785
3604
|
function assertWorkspaceDeletable(input) {
|
|
2786
3605
|
if (input.workspaceCountForAccount <= 1) {
|
|
2787
|
-
throw new
|
|
3606
|
+
throw new HTTPException11(409, { message: "cannot delete the account's only workspace" });
|
|
2788
3607
|
}
|
|
2789
3608
|
if (input.activeSessionCount > 0) {
|
|
2790
|
-
throw new
|
|
3609
|
+
throw new HTTPException11(409, {
|
|
2791
3610
|
message: "stop the workspace's running sessions before deleting it"
|
|
2792
3611
|
});
|
|
2793
3612
|
}
|
|
2794
3613
|
}
|
|
2795
3614
|
export {
|
|
2796
3615
|
MARKETING_SOCIAL_PACK_ID,
|
|
3616
|
+
MAX_CHECKS_PER_RIG,
|
|
3617
|
+
MAX_CREDENTIAL_HOOKS_PER_RIG,
|
|
3618
|
+
MAX_DEFAULT_VARIABLE_SETS_PER_RIG,
|
|
2797
3619
|
MAX_ENVIRONMENTS_PER_WORKSPACE,
|
|
3620
|
+
MAX_RIGS_PER_WORKSPACE,
|
|
2798
3621
|
MAX_VARIABLES_PER_ENVIRONMENT,
|
|
2799
3622
|
acceptSessionUserMessage,
|
|
3623
|
+
activateRigVersionForApi,
|
|
3624
|
+
appendRigSetupCommand,
|
|
2800
3625
|
applyCapabilityEnablement,
|
|
2801
3626
|
assertAllowedEnvironmentVariableName,
|
|
3627
|
+
assertAllowedVariableSetVariableName,
|
|
2802
3628
|
assertConfiguredModel,
|
|
2803
3629
|
assertPackSandboxImageCompatible,
|
|
2804
3630
|
assertWorkspaceDeletable,
|
|
2805
3631
|
assertWorkspaceMemberRemovable,
|
|
3632
|
+
assertWorkspaceModelPolicyAllows,
|
|
2806
3633
|
buildCapabilityCatalog,
|
|
2807
3634
|
buildFleetContextForSession,
|
|
2808
3635
|
buildMarketingDailyAnalysisAgentConfig,
|
|
2809
3636
|
checkLimit,
|
|
3637
|
+
classifyRigVerificationOutcome,
|
|
2810
3638
|
createAndStartSession,
|
|
2811
3639
|
createCatalogItem,
|
|
3640
|
+
createRigForApi,
|
|
3641
|
+
createRigVersionForApi,
|
|
2812
3642
|
createSessionForRequest,
|
|
2813
3643
|
createValidatedScheduledTask,
|
|
3644
|
+
deleteRigForApi,
|
|
2814
3645
|
disableCapability,
|
|
2815
3646
|
discoverMcpRegistryCapabilities,
|
|
2816
3647
|
enableCapability,
|
|
@@ -2821,6 +3652,8 @@ export {
|
|
|
2821
3652
|
isUserMember,
|
|
2822
3653
|
listCapabilityPacks,
|
|
2823
3654
|
listFleet,
|
|
3655
|
+
listRigChangesForApi,
|
|
3656
|
+
listRigVersionsForApi,
|
|
2824
3657
|
listWorkspaceCapabilityPacks,
|
|
2825
3658
|
manualScheduledTaskTriggerUsageKey,
|
|
2826
3659
|
manualScheduledTaskTriggerWorkflowId,
|
|
@@ -2830,23 +3663,32 @@ export {
|
|
|
2830
3663
|
normalizeResources,
|
|
2831
3664
|
officialMcpRegistryUrl,
|
|
2832
3665
|
postUserMessageTurn,
|
|
3666
|
+
promoteSetupAppendChange,
|
|
3667
|
+
promoteVerifiedDefinitionEditChangeForApi,
|
|
3668
|
+
proposeRigChangeForApi,
|
|
2833
3669
|
provisionSandbox,
|
|
3670
|
+
readSessionLineage,
|
|
2834
3671
|
reasoningEffortForSession,
|
|
2835
|
-
|
|
3672
|
+
recordRigAuditEvent,
|
|
3673
|
+
recordVariableSetAuditEvent,
|
|
2836
3674
|
recordWorkspaceUsage,
|
|
2837
3675
|
relayConfigFromSettings,
|
|
2838
3676
|
relayDialBaseFromSettings,
|
|
2839
3677
|
requireAccessContext,
|
|
2840
3678
|
requireAccessGrant,
|
|
2841
3679
|
requireEnvironmentEncryption,
|
|
2842
|
-
requireEnvironmentForApi,
|
|
2843
3680
|
requireLimit,
|
|
2844
3681
|
requirePermission,
|
|
2845
3682
|
requireQueuedTurnForApi,
|
|
3683
|
+
requireRigChangeForApi,
|
|
3684
|
+
requireRigForApi,
|
|
2846
3685
|
requireScheduledTaskForApi,
|
|
3686
|
+
requireVariableSetEncryption,
|
|
3687
|
+
requireVariableSetForApi,
|
|
2847
3688
|
resolveCapabilityPack,
|
|
2848
3689
|
resolveMemberSubjectId,
|
|
2849
3690
|
restoreScheduledTask,
|
|
3691
|
+
rigActorForGrant,
|
|
2850
3692
|
routingEnabled,
|
|
2851
3693
|
runOnSandbox,
|
|
2852
3694
|
scheduledTaskTemporalScheduleId,
|
|
@@ -2859,13 +3701,14 @@ export {
|
|
|
2859
3701
|
swapActiveSandbox,
|
|
2860
3702
|
syncCreatedScheduledTask,
|
|
2861
3703
|
syncUpdatedScheduledTask,
|
|
3704
|
+
updateRigForApi,
|
|
2862
3705
|
updateSessionTitle,
|
|
2863
|
-
validateEnvironmentAttachment,
|
|
2864
3706
|
validateFileResources,
|
|
2865
3707
|
validateGitHubRepositorySelection,
|
|
2866
3708
|
validateGitHubRepositorySelectionShape,
|
|
2867
3709
|
validateMcpCapabilityConnection,
|
|
2868
3710
|
validateToolRefs,
|
|
3711
|
+
validateVariableSetAttachment,
|
|
2869
3712
|
validatedScheduledTaskUpdate,
|
|
2870
3713
|
withDefaultEnabledCapabilityMcpTools,
|
|
2871
3714
|
workflowIdForSession,
|