@opengeni/core 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 ? { id: sandbox.id, kind: sandbox.kind, name: sandbox.name, enrollmentId: sandbox.enrollmentId } : null;
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 { activeSandboxId: pointer.activeSandboxId, activeEpoch: pointer.activeEpoch, sandboxes: entries };
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 { ok: false, reason: `sandbox ${target} not found in this workspace` };
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 { ok: false, reason: `selfhosted sandbox ${target} has no enrollment` };
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 { ok: false, reason: `enrollment for sandbox ${target} not found` };
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 { ok: false, reason: `sandbox ${target} is ${probe.liveness}; cannot attach to a non-online machine` };
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 { swapped: false, activeSandboxId: pointer2.activeSandboxId, activeEpoch: pointer2.activeEpoch, reason: resolved.reason };
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 { swapped: true, activeSandboxId: pointer2.activeSandboxId, activeEpoch: pointer2.activeEpoch };
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 { swapped: true, activeSandboxId: result.pointer.activeSandboxId, activeEpoch: result.pointer.activeEpoch };
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 { target, kind: op.kind, ok: false, reason: `sandbox ${target} not found in this workspace` };
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({ cmd: op.cmd, ...op.workdir ? { workdir: op.workdir } : {} });
265
- return { target, kind: "exec", ok: true, stdout: res.stdout, stderr: res.stderr, exitCode: res.exitCode };
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. Its box is materialized when first swapped-to; the session's own group box remains the default until then."
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 { verifyDelegatedAccessToken } from "@opengeni/contracts";
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
- return permissions.includes(permission) || permissions.includes("workspace:admin");
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 findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
472
+ const apiKey = await apiKeyAccessContext(c, deps, "managed");
388
473
  if (apiKey) {
389
- const accountPermissions = apiKey.workspaceId ? apiKey.permissions.filter((permission) => permission === "billing:read" || permission === "billing:manage") : apiKey.permissions;
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
- accountId: payload.accountId,
438
- subjectId: payload.subjectId,
439
- ...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
440
- permissions: payload.permissions
441
- }],
442
- workspaceGrants: [{
443
- workspaceId: payload.workspaceId,
444
- accountId: payload.accountId,
445
- subjectId: payload.subjectId,
446
- ...payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {},
447
- permissions: payload.permissions,
448
- // sessionId is worker-asserted (HMAC-signed token claim), not agent
449
- // controlled; it scopes session-bound MCP tools such as goal management.
450
- metadata: { delegated: true, ...payload.sessionId ? { sessionId: payload.sessionId } : {} }
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, { message: decision.message });
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({ db: deps.db, settings: deps.settings, workspaceId: input.workspaceId, model: input.model }) : false;
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("max_monthly_cost_micros_per_account", `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`);
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("max_workspaces_per_account", `workspace limit reached (${limits.maxWorkspacesPerAccount})`);
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("max_api_keys_per_workspace", `API key limit reached (${limits.maxApiKeysPerWorkspace})`);
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("max_schedules_per_workspace", `scheduled task limit reached (${limits.maxSchedulesPerWorkspace})`);
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("max_file_upload_bytes", `file upload exceeds static limit of ${limits.maxFileUploadBytes} bytes`);
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("max_monthly_agent_runs_per_workspace", `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`);
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("max_monthly_tokens_per_workspace", `monthly token limit reached (${limits.maxMonthlyTokensPerWorkspace})`);
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("max_document_indexed_chunks_per_workspace", `monthly document indexing limit reached (${limits.maxDocumentIndexedChunksPerWorkspace} chunks)`);
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 === "file:upload" || action === "document:index";
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
- decryptEnvironmentValue,
770
+ decryptVariableSetValue,
627
771
  decryptedCapabilityHeaders,
628
772
  disableCapabilityInstallation,
629
773
  enableCapabilityInstallation,
630
774
  enablePackInstallation,
631
- encryptEnvironmentValue,
775
+ encryptVariableSetValue,
632
776
  getCapabilityCatalogItem,
633
777
  getCapabilityInstallation,
634
778
  getConnectionMetadata,
635
779
  getPackInstallation,
636
780
  getStoredCapabilityHeaderCiphertext,
637
- getWorkspaceEnvironment as getWorkspaceEnvironment2,
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 assertAllowedEnvironmentVariableName(name) {
828
+ function assertAllowedVariableSetVariableName(name) {
686
829
  if (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
687
- throw new HTTPException4(422, { message: `reserved environment variable name: ${name}` });
830
+ throw new HTTPException4(422, {
831
+ message: `reserved variable set variable name / reserved environment variable name: ${name}`
832
+ });
688
833
  }
689
834
  }
690
- function requireEnvironmentEncryption(settings) {
835
+ var assertAllowedEnvironmentVariableName = assertAllowedVariableSetVariableName;
836
+ function requireVariableSetEncryption(settings) {
691
837
  const key = environmentsEncryptionKeyBytes(settings);
692
838
  if (!key) {
693
- throw new HTTPException4(503, { message: "workspace environments require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY" });
839
+ throw new HTTPException4(503, {
840
+ message: "variable sets require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY"
841
+ });
694
842
  }
695
843
  return key;
696
844
  }
697
- async function requireEnvironmentForApi(db, workspaceId, environmentId) {
698
- const environment = await getWorkspaceEnvironment(db, workspaceId, environmentId);
699
- if (!environment) {
700
- throw new HTTPException4(404, { message: "environment not found" });
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 environment;
851
+ return variableSet;
703
852
  }
704
- async function validateEnvironmentAttachment(deps, grant, workspaceId, environmentId, options = {}) {
705
- requireEnvironmentEncryption(deps.settings);
853
+ async function validateVariableSetAttachment(deps, grant, workspaceId, variableSetId, options = {}) {
854
+ requireVariableSetEncryption(deps.settings);
706
855
  if (!options.preauthorized) {
707
- requirePermission(grant, "environments:use");
856
+ requirePermission(grant, "variable-sets:use");
708
857
  }
709
- const environment = await getWorkspaceEnvironment(deps.db, workspaceId, environmentId);
710
- if (!environment) {
711
- throw new HTTPException4(422, { message: "unknown environmentId" });
858
+ const variableSet = await getVariableSet(deps.db, workspaceId, variableSetId);
859
+ if (!variableSet) {
860
+ throw new HTTPException4(422, { message: "unknown variableSetId" });
712
861
  }
713
- return environment;
862
+ return variableSet;
714
863
  }
715
- async function recordEnvironmentAuditEvent(db, input) {
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: "workspace_environment",
722
- targetId: input.environmentId,
870
+ targetType: "workspace_variable_set",
871
+ targetId: input.variableSetId,
723
872
  metadata: {
724
- environmentId: input.environmentId,
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 { getWorkspacePack, listPackInstallations, listWorkspacePacks } from "@opengeni/db";
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: ["instagram_basic", "instagram_manage_insights", "pages_read_engagement", "pages_show_list"],
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: ["https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/yt-analytics.readonly"],
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(capabilityInstallations.map((installation) => [installation.capabilityId, installation]));
961
- const activePackIds = new Set(packInstallations.filter((installation) => installation.status === "active").map((installation) => installation.packId));
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((pack) => packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual")),
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((item) => applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds)).sort(compareCatalogItems);
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, { message: "packs are managed by OpenGeni and cannot be manually created" });
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(input.db, input.workspaceId, input.settings, input.capabilityId);
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, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled" });
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, encryptEnvironmentValue(key, value)])
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 storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
1040
- const requestedEnvironmentId = input.payload.environmentId;
1041
- const environmentId = requestedEnvironmentId ?? storedEnvironmentId;
1042
- if (pack.environment?.required && !environmentId) {
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 environment attachment; pass environmentId`
1222
+ message: `pack ${packId} requires an variableSet attachment; pass variableSetId`
1045
1223
  });
1046
1224
  }
1047
- if (environmentId) {
1048
- if (requestedEnvironmentId) {
1049
- const environment = await validateEnvironmentAttachment(
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
- requestedEnvironmentId
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, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
1237
+ throw new HTTPException6(422, {
1238
+ message: `variable set is missing required variable(s): ${missing.join(", ")}`
1239
+ });
1058
1240
  }
1059
1241
  } else {
1060
- const environment = await getWorkspaceEnvironment2(input.db, input.workspaceId, environmentId);
1061
- if (!environment) {
1242
+ const variableSet = await getVariableSet2(input.db, input.workspaceId, variableSetId);
1243
+ if (!variableSet) {
1062
1244
  throw new HTTPException6(422, {
1063
- message: `the stored environment attachment for pack ${packId} no longer exists; re-enable it with environmentId`
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.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
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, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
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
- ...environmentId ? { environmentId } : {}
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(input.db, input.workspaceId, item.id);
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(Object.entries(storedCiphertext).map(([name, value]) => [name, decryptEnvironmentValue(key, value)]));
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, { message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers` });
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, { message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters` });
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, { message: `credential header ${name} contains forbidden control characters` });
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, { message: "subject-owned connection refs are not supported for agent runtime use yet" });
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, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef" });
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(input.db, input.workspaceId, normalized.connectionId, input.grant.subjectId);
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, { message: "connectionRef.connectionId does not reference a visible connection" });
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, { message: "agent runtime connection refs must reference workspace-shared connections in I1" });
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, { message: `connectionRef.connectionId is not active (${connection.status})` });
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, { message: "connectionRef.providerDomain does not match the referenced connection" });
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, { message: "connectionRef.kind does not match the referenced connection" });
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, { message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY" });
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, { message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled" });
1444
+ throw new HTTPException6(422, {
1445
+ message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled"
1446
+ });
1223
1447
  }
1224
1448
  try {
1225
1449
  const result = await probe({
@@ -1238,14 +1462,17 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
1238
1462
  };
1239
1463
  } catch (error) {
1240
1464
  throw new HTTPException6(422, {
1241
- message: `MCP capability "${item.name}" could not be enabled because OpenGeni could not initialize ${item.endpointUrl}: ${mcpProbeErrorMessage(error)}`
1465
+ message: `MCP capability "${item.name}" could not be enabled because ${mcpProbeErrorMessage(error, item.endpointUrl)}`
1242
1466
  });
1243
1467
  }
1244
1468
  }
1245
1469
  async function probeStreamableHttpMcpServer(input) {
1246
1470
  const controller = new AbortController();
1247
1471
  const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
1248
- const client = new Client({ name: "opengeni-capability-probe", version: "0.1.0" }, { capabilities: {} });
1472
+ const client = new Client(
1473
+ { name: "opengeni-capability-probe", version: "0.1.0" },
1474
+ { capabilities: {} }
1475
+ );
1249
1476
  try {
1250
1477
  const transport = new StreamableHTTPClientTransport(new URL(input.url), {
1251
1478
  requestInit: {
@@ -1253,25 +1480,49 @@ async function probeStreamableHttpMcpServer(input) {
1253
1480
  ...input.headers ? { headers: input.headers } : {}
1254
1481
  }
1255
1482
  });
1256
- await client.connect(transport, { timeout: input.timeoutMs, maxTotalTimeout: input.timeoutMs });
1257
- const tools = await client.listTools(void 0, { timeout: input.timeoutMs, maxTotalTimeout: input.timeoutMs });
1483
+ await client.connect(transport, {
1484
+ timeout: input.timeoutMs,
1485
+ maxTotalTimeout: input.timeoutMs
1486
+ });
1487
+ const tools = await client.listTools(void 0, {
1488
+ timeout: input.timeoutMs,
1489
+ maxTotalTimeout: input.timeoutMs
1490
+ });
1258
1491
  return { toolCount: tools.tools.length };
1259
1492
  } finally {
1260
1493
  clearTimeout(timeout);
1261
1494
  await client.close().catch(() => void 0);
1262
1495
  }
1263
1496
  }
1264
- function mcpProbeErrorMessage(error) {
1497
+ function mcpProbeErrorMessage(error, endpointUrl) {
1265
1498
  const message = error instanceof Error ? error.message : String(error);
1266
- return message.replace(/\s+/g, " ").trim().slice(0, 500) || "unknown error";
1499
+ const normalized = message.replace(/\s+/g, " ").trim();
1500
+ if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
1501
+ normalized
1502
+ )) {
1503
+ return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
1504
+ }
1505
+ return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
1267
1506
  }
1268
1507
  async function disableCapability(input) {
1269
- const item = await requireCatalogItem(input.db, input.workspaceId, input.settings, input.capabilityId);
1508
+ const item = await requireCatalogItem(
1509
+ input.db,
1510
+ input.workspaceId,
1511
+ input.settings,
1512
+ input.capabilityId
1513
+ );
1270
1514
  if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
1271
- throw new HTTPException6(409, { message: "built-in and configured capabilities are always available; remove them from configuration to disable them" });
1515
+ throw new HTTPException6(409, {
1516
+ message: "built-in and configured capabilities are always available; remove them from configuration to disable them"
1517
+ });
1272
1518
  }
1273
1519
  if (item.kind === "pack") {
1274
- await updatePackInstallationStatus(input.db, input.workspaceId, packIdFromCapabilityId(item.id), "disabled").catch(() => void 0);
1520
+ await updatePackInstallationStatus(
1521
+ input.db,
1522
+ input.workspaceId,
1523
+ packIdFromCapabilityId(item.id),
1524
+ "disabled"
1525
+ ).catch(() => void 0);
1275
1526
  if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
1276
1527
  await enableCapabilityInstallation(input.db, {
1277
1528
  accountId: input.accountId,
@@ -1302,16 +1553,18 @@ function settingsWithMcpCapabilityServers(settings, enabled) {
1302
1553
  if (headers === "unavailable" && !server.connectionRef) {
1303
1554
  return [];
1304
1555
  }
1305
- return [{
1306
- id: server.id,
1307
- name: server.name,
1308
- url: server.url,
1309
- ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
1310
- ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
1311
- cacheToolsList: server.cacheToolsList ?? false,
1312
- ...headers && headers !== "unavailable" ? { headers } : {},
1313
- ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
1314
- }];
1556
+ return [
1557
+ {
1558
+ id: server.id,
1559
+ name: server.name,
1560
+ url: server.url,
1561
+ ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
1562
+ ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
1563
+ cacheToolsList: server.cacheToolsList ?? false,
1564
+ ...headers && headers !== "unavailable" ? { headers } : {},
1565
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
1566
+ }
1567
+ ];
1315
1568
  });
1316
1569
  return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
1317
1570
  }
@@ -1365,7 +1618,10 @@ async function discoverMcpRegistryCapabilities(input) {
1365
1618
  async function fetchMcpRegistryPage(url, options = {}) {
1366
1619
  const fetchImpl = options.fetchImpl ?? fetch;
1367
1620
  const controller = new AbortController();
1368
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? mcpRegistryFetchTimeoutMs);
1621
+ const timeout = setTimeout(
1622
+ () => controller.abort(),
1623
+ options.timeoutMs ?? mcpRegistryFetchTimeoutMs
1624
+ );
1369
1625
  try {
1370
1626
  const response = await fetchImpl(url, { signal: controller.signal });
1371
1627
  if (!response.ok) {
@@ -1422,28 +1678,30 @@ function packCatalogItem(pack, source) {
1422
1678
  });
1423
1679
  }
1424
1680
  function configuredMcpCatalogItems(settings) {
1425
- return settings.mcpServers.map((server) => CapabilityCatalogItem.parse({
1426
- id: `mcp:${server.id}`,
1427
- kind: "mcp",
1428
- source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
1429
- name: server.name ?? server.id,
1430
- description: firstPartyMcpDescription(server.id),
1431
- category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
1432
- tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
1433
- endpointUrl: server.url,
1434
- tools: [{ kind: "mcp", id: server.id }],
1435
- runtime: {
1436
- available: true,
1437
- mcpServerId: server.id,
1438
- transport: "streamable-http",
1439
- notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
1440
- },
1441
- metadata: {
1442
- mcpServerId: server.id,
1443
- allowedTools: server.allowedTools ?? [],
1444
- cacheToolsList: server.cacheToolsList
1445
- }
1446
- }));
1681
+ return settings.mcpServers.map(
1682
+ (server) => CapabilityCatalogItem.parse({
1683
+ id: `mcp:${server.id}`,
1684
+ kind: "mcp",
1685
+ source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
1686
+ name: server.name ?? server.id,
1687
+ description: firstPartyMcpDescription(server.id),
1688
+ category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
1689
+ tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
1690
+ endpointUrl: server.url,
1691
+ tools: [{ kind: "mcp", id: server.id }],
1692
+ runtime: {
1693
+ available: true,
1694
+ mcpServerId: server.id,
1695
+ transport: "streamable-http",
1696
+ notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
1697
+ },
1698
+ metadata: {
1699
+ mcpServerId: server.id,
1700
+ allowedTools: server.allowedTools ?? [],
1701
+ cacheToolsList: server.cacheToolsList
1702
+ }
1703
+ })
1704
+ );
1447
1705
  }
1448
1706
  function platformApiCatalogItems() {
1449
1707
  return [
@@ -1479,46 +1737,56 @@ function platformApiCatalogItems() {
1479
1737
  tags: ["api", "schedules", "agents"],
1480
1738
  endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks"
1481
1739
  }
1482
- ].map((item) => CapabilityCatalogItem.parse({
1483
- id: item.id,
1484
- name: item.name,
1485
- description: item.description,
1486
- category: item.category,
1487
- tags: item.tags,
1488
- kind: "api",
1489
- source: "built_in",
1490
- runtime: {
1491
- available: true,
1492
- notes: "Available through the OpenGeni API."
1493
- },
1494
- metadata: {
1495
- endpointPath: item.endpointPath
1496
- }
1497
- }));
1740
+ ].map(
1741
+ (item) => CapabilityCatalogItem.parse({
1742
+ id: item.id,
1743
+ name: item.name,
1744
+ description: item.description,
1745
+ category: item.category,
1746
+ tags: item.tags,
1747
+ kind: "api",
1748
+ source: "built_in",
1749
+ runtime: {
1750
+ available: true,
1751
+ notes: "Available through the OpenGeni API."
1752
+ },
1753
+ metadata: {
1754
+ endpointPath: item.endpointPath
1755
+ }
1756
+ })
1757
+ );
1498
1758
  }
1499
1759
  async function discoverBundledSkills() {
1500
- const skillsDir = new URL("../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/", import.meta.url);
1760
+ const skillsDir = new URL(
1761
+ "../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
1762
+ import.meta.url
1763
+ );
1501
1764
  try {
1502
1765
  const entries = await readdir(skillsDir, { withFileTypes: true });
1503
- const skills = await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1504
- const skill = await readSkillMetadata(new URL(`${entry.name}/SKILL.md`, skillsDir), entry.name);
1505
- return CapabilityCatalogItem.parse({
1506
- id: `skill:${entry.name}`,
1507
- kind: "skill",
1508
- source: "built_in",
1509
- name: skill.name,
1510
- description: skill.description,
1511
- category: skill.category,
1512
- tags: ["skill", skill.category],
1513
- runtime: {
1514
- available: true,
1515
- notes: "Bundled into the sandbox skill library."
1516
- },
1517
- metadata: {
1518
- path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
1519
- }
1520
- });
1521
- }));
1766
+ const skills = await Promise.all(
1767
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1768
+ const skill = await readSkillMetadata(
1769
+ new URL(`${entry.name}/SKILL.md`, skillsDir),
1770
+ entry.name
1771
+ );
1772
+ return CapabilityCatalogItem.parse({
1773
+ id: `skill:${entry.name}`,
1774
+ kind: "skill",
1775
+ source: "built_in",
1776
+ name: skill.name,
1777
+ description: skill.description,
1778
+ category: skill.category,
1779
+ tags: ["skill", skill.category],
1780
+ runtime: {
1781
+ available: true,
1782
+ notes: "Bundled into the sandbox skill library."
1783
+ },
1784
+ metadata: {
1785
+ path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
1786
+ }
1787
+ });
1788
+ })
1789
+ );
1522
1790
  return skills;
1523
1791
  } catch {
1524
1792
  return [];
@@ -1595,7 +1863,11 @@ function firstPartyMcpDescription(id) {
1595
1863
  return null;
1596
1864
  }
1597
1865
  function generatedCapabilityId(payload) {
1598
- const source = [payload.kind, payload.name, payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""].join(":");
1866
+ const source = [
1867
+ payload.kind,
1868
+ payload.name,
1869
+ payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? ""
1870
+ ].join(":");
1599
1871
  return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
1600
1872
  }
1601
1873
  function publicRegistryCapabilityId(name, version, endpointUrl) {
@@ -1633,7 +1905,9 @@ function mcpRegistryEntryToCatalogItem(entry) {
1633
1905
  if (official?.isLatest === false) {
1634
1906
  return null;
1635
1907
  }
1636
- const remote = server.remotes?.find((candidate) => candidate.type === "streamable-http" && candidate.url);
1908
+ const remote = server.remotes?.find(
1909
+ (candidate) => candidate.type === "streamable-http" && candidate.url
1910
+ );
1637
1911
  const endpointUrl = validUrl(remote?.url);
1638
1912
  if (!remote || !endpointUrl) {
1639
1913
  return null;
@@ -1650,7 +1924,12 @@ function mcpRegistryEntryToCatalogItem(entry) {
1650
1924
  name: server.title || server.name,
1651
1925
  description: server.description ?? null,
1652
1926
  category: "public-mcp",
1653
- tags: ["mcp", "public", "registry", ...requiredHeaders.length ? ["requires-credentials"] : []],
1927
+ tags: [
1928
+ "mcp",
1929
+ "public",
1930
+ "registry",
1931
+ ...requiredHeaders.length ? ["requires-credentials"] : []
1932
+ ],
1654
1933
  homepageUrl,
1655
1934
  endpointUrl,
1656
1935
  installUrl: homepageUrl,
@@ -1728,6 +2007,367 @@ function storedConnectionRef(config) {
1728
2007
  return !!ref && typeof ref === "object" && !Array.isArray(ref) && typeof ref.providerDomain === "string";
1729
2008
  }
1730
2009
 
2010
+ // src/rigs/index.ts
2011
+ import {
2012
+ activateRigVersion,
2013
+ countRigs,
2014
+ createRig,
2015
+ createRigChange,
2016
+ createRigVersion,
2017
+ createRigVersionForChangePromotion,
2018
+ deleteRigIfNoActiveSessions,
2019
+ getRig,
2020
+ getRigByName,
2021
+ getRigChange,
2022
+ getRigVersion,
2023
+ getVariableSet as getVariableSet3,
2024
+ listRigChanges,
2025
+ listRigVersions,
2026
+ recordAuditEvent as recordAuditEvent2,
2027
+ RigActiveVersionChangedError,
2028
+ RigChangeTransitionError,
2029
+ updateRig
2030
+ } from "@opengeni/db";
2031
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
2032
+ var MAX_RIGS_PER_WORKSPACE = 50;
2033
+ var MAX_CHECKS_PER_RIG = 100;
2034
+ var MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
2035
+ var MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
2036
+ async function recordRigAuditEvent(db, input) {
2037
+ await recordAuditEvent2(db, {
2038
+ accountId: input.grant.accountId,
2039
+ workspaceId: input.grant.workspaceId,
2040
+ subjectId: input.grant.subjectId,
2041
+ action: input.action,
2042
+ targetType: "rig",
2043
+ targetId: input.rigId,
2044
+ metadata: { rigId: input.rigId, ...input.metadata ?? {} }
2045
+ });
2046
+ }
2047
+ function rigActorForGrant(grant) {
2048
+ return `user:${grant.subjectId}`;
2049
+ }
2050
+ async function requireRigForApi(db, workspaceId, rigId) {
2051
+ const rig = await getRig(db, workspaceId, rigId);
2052
+ if (!rig) {
2053
+ throw new HTTPException7(404, { message: "rig not found" });
2054
+ }
2055
+ return rig;
2056
+ }
2057
+ async function requireRigChangeForApi(db, workspaceId, rigId, changeId) {
2058
+ const change = await getRigChange(db, workspaceId, changeId);
2059
+ if (!change || change.rigId !== rigId) {
2060
+ throw new HTTPException7(404, { message: "rig change not found" });
2061
+ }
2062
+ return change;
2063
+ }
2064
+ function trimmedRigName(name) {
2065
+ const trimmed = name.trim();
2066
+ if (!trimmed) {
2067
+ throw new HTTPException7(422, { message: "rig name is required" });
2068
+ }
2069
+ return trimmed;
2070
+ }
2071
+ function assertUniqueCheckNames(checks) {
2072
+ if (!checks) {
2073
+ return;
2074
+ }
2075
+ const seen = /* @__PURE__ */ new Set();
2076
+ for (const check of checks) {
2077
+ if (seen.has(check.name)) {
2078
+ throw new HTTPException7(422, { message: `duplicate rig check name: ${check.name}` });
2079
+ }
2080
+ seen.add(check.name);
2081
+ }
2082
+ }
2083
+ async function assertVariableSetsExist(db, workspaceId, ids) {
2084
+ if (!ids || ids.length === 0) {
2085
+ return;
2086
+ }
2087
+ const unique = [...new Set(ids)];
2088
+ for (const id of unique) {
2089
+ const variableSet = await getVariableSet3(db, workspaceId, id);
2090
+ if (!variableSet) {
2091
+ throw new HTTPException7(422, { message: `unknown defaultVariableSetId: ${id}` });
2092
+ }
2093
+ }
2094
+ }
2095
+ async function createRigForApi(deps, grant, payload) {
2096
+ const workspaceId = grant.workspaceId;
2097
+ const name = trimmedRigName(payload.name);
2098
+ assertUniqueCheckNames(payload.checks);
2099
+ await assertVariableSetsExist(deps.db, workspaceId, payload.defaultVariableSetIds);
2100
+ if (await countRigs(deps.db, workspaceId) >= MAX_RIGS_PER_WORKSPACE) {
2101
+ throw new HTTPException7(422, {
2102
+ message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`
2103
+ });
2104
+ }
2105
+ if (await getRigByName(deps.db, workspaceId, name)) {
2106
+ throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
2107
+ }
2108
+ const createdBy = rigActorForGrant(grant);
2109
+ const rig = await createRig(deps.db, {
2110
+ accountId: grant.accountId,
2111
+ workspaceId,
2112
+ name,
2113
+ description: payload.description ?? null,
2114
+ createdBy,
2115
+ initialVersion: {
2116
+ image: payload.image ?? null,
2117
+ setupScript: payload.setupScript ?? null,
2118
+ checks: payload.checks,
2119
+ credentialHooks: payload.credentialHooks,
2120
+ defaultVariableSetIds: payload.defaultVariableSetIds,
2121
+ changelog: "Initial version",
2122
+ createdBy
2123
+ }
2124
+ });
2125
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.created", rigId: rig.id });
2126
+ return rig;
2127
+ }
2128
+ async function updateRigForApi(deps, grant, rig, payload) {
2129
+ const workspaceId = grant.workspaceId;
2130
+ const name = payload.name !== void 0 ? trimmedRigName(payload.name) : void 0;
2131
+ if (name !== void 0 && name !== rig.name) {
2132
+ const existing = await getRigByName(deps.db, workspaceId, name);
2133
+ if (existing && existing.id !== rig.id) {
2134
+ throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
2135
+ }
2136
+ }
2137
+ const updated = await updateRig(deps.db, workspaceId, rig.id, {
2138
+ ...name !== void 0 ? { name } : {},
2139
+ ...payload.description !== void 0 ? { description: payload.description } : {}
2140
+ });
2141
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.updated", rigId: rig.id });
2142
+ return updated;
2143
+ }
2144
+ async function deleteRigForApi(deps, grant, rig) {
2145
+ const workspaceId = grant.workspaceId;
2146
+ const deleted = await deleteRigIfNoActiveSessions(deps.db, workspaceId, rig.id);
2147
+ if (deleted.activeSessionCount > 0) {
2148
+ throw new HTTPException7(409, {
2149
+ message: `rig is referenced by ${deleted.activeSessionCount} active session(s); it cannot be deleted`
2150
+ });
2151
+ }
2152
+ if (!deleted.deleted) {
2153
+ throw new HTTPException7(404, { message: "rig not found" });
2154
+ }
2155
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.deleted", rigId: rig.id });
2156
+ }
2157
+ async function proposeRigChangeForApi(deps, grant, rig, request, options = {}) {
2158
+ const workspaceId = grant.workspaceId;
2159
+ if (!rig.activeVersion) {
2160
+ throw new HTTPException7(422, { message: "rig has no active version to base a change on" });
2161
+ }
2162
+ if (request.kind === "definition_edit") {
2163
+ assertUniqueCheckNames(request.payload.checks);
2164
+ await assertVariableSetsExist(
2165
+ deps.db,
2166
+ workspaceId,
2167
+ request.payload.defaultVariableSetIds ?? void 0
2168
+ );
2169
+ }
2170
+ const change = await createRigChange(deps.db, {
2171
+ accountId: grant.accountId,
2172
+ workspaceId,
2173
+ rigId: rig.id,
2174
+ baseVersionId: rig.activeVersion.id,
2175
+ kind: request.kind,
2176
+ payload: request.payload,
2177
+ proposedBy: options.proposedBy ?? rigActorForGrant(grant)
2178
+ });
2179
+ await recordRigAuditEvent(deps.db, {
2180
+ grant,
2181
+ action: "rig.change.proposed",
2182
+ rigId: rig.id,
2183
+ metadata: { changeId: change.id, kind: change.kind }
2184
+ });
2185
+ return change;
2186
+ }
2187
+ function classifyRigVerificationOutcome(input) {
2188
+ if (input.infraError) {
2189
+ return { status: "failed", action: "retryable_failure" };
2190
+ }
2191
+ if (!input.passed) {
2192
+ return { status: "rejected", action: "reject" };
2193
+ }
2194
+ if (input.kind === "setup_append") {
2195
+ return { status: "merged", action: "auto_promote" };
2196
+ }
2197
+ return { status: "proposed", action: "await_manage_promote" };
2198
+ }
2199
+ function appendRigSetupCommand(baseSetupScript, command) {
2200
+ const base = (baseSetupScript ?? "").trimEnd();
2201
+ return base ? `${base}
2202
+ ${command}` : command;
2203
+ }
2204
+ async function promoteChangeWithActiveCas(deps, workspaceId, rigId, changeId, input) {
2205
+ try {
2206
+ return await createRigVersionForChangePromotion(deps.db, workspaceId, rigId, changeId, input);
2207
+ } catch (error) {
2208
+ if (error instanceof RigActiveVersionChangedError) {
2209
+ throw new HTTPException7(409, {
2210
+ message: `rig moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`
2211
+ });
2212
+ }
2213
+ if (error instanceof RigChangeTransitionError) {
2214
+ throw new HTTPException7(409, { message: error.message });
2215
+ }
2216
+ throw error;
2217
+ }
2218
+ }
2219
+ async function promoteSetupAppendChange(deps, grant, rig, change) {
2220
+ if (change.kind !== "setup_append") {
2221
+ throw new HTTPException7(422, {
2222
+ message: "only setup_append changes auto-promote through this path"
2223
+ });
2224
+ }
2225
+ if (change.status !== "proposed" && change.status !== "verifying") {
2226
+ throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
2227
+ }
2228
+ if (!change.baseVersionId) {
2229
+ throw new HTTPException7(422, { message: "rig change has no base version" });
2230
+ }
2231
+ const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
2232
+ if (!base) {
2233
+ throw new HTTPException7(404, { message: "base rig version not found" });
2234
+ }
2235
+ const payload = change.payload;
2236
+ if (typeof payload.command !== "string" || !payload.command.trim()) {
2237
+ throw new HTTPException7(422, { message: "setup_append change is missing command" });
2238
+ }
2239
+ const { version, change: updated } = await promoteChangeWithActiveCas(
2240
+ deps,
2241
+ grant.workspaceId,
2242
+ rig.id,
2243
+ change.id,
2244
+ {
2245
+ expectedActiveVersionId: change.baseVersionId,
2246
+ image: base.image,
2247
+ setupScript: appendRigSetupCommand(base.setupScript, payload.command),
2248
+ checks: base.checks,
2249
+ credentialHooks: base.credentialHooks,
2250
+ defaultVariableSetIds: base.defaultVariableSetIds,
2251
+ changelog: typeof payload.note === "string" && payload.note.trim() ? payload.note : "Verified setup append",
2252
+ createdBy: change.proposedBy ?? rigActorForGrant(grant)
2253
+ }
2254
+ );
2255
+ await recordRigAuditEvent(deps.db, {
2256
+ grant,
2257
+ action: "rig.change.merged",
2258
+ rigId: rig.id,
2259
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2260
+ });
2261
+ await recordRigAuditEvent(deps.db, {
2262
+ grant,
2263
+ action: "rig.version.promoted",
2264
+ rigId: rig.id,
2265
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2266
+ });
2267
+ return { change: updated, version };
2268
+ }
2269
+ async function promoteVerifiedDefinitionEditChangeForApi(deps, grant, rig, change) {
2270
+ if (change.kind !== "definition_edit") {
2271
+ throw new HTTPException7(422, { message: "only definition_edit changes use explicit promote" });
2272
+ }
2273
+ if (change.status !== "proposed") {
2274
+ throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
2275
+ }
2276
+ if (change.verification?.passed !== true) {
2277
+ throw new HTTPException7(422, {
2278
+ message: "definition_edit change must pass verification before promote"
2279
+ });
2280
+ }
2281
+ if (!change.baseVersionId) {
2282
+ throw new HTTPException7(422, { message: "rig change has no base version" });
2283
+ }
2284
+ const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
2285
+ if (!base) {
2286
+ throw new HTTPException7(404, { message: "base rig version not found" });
2287
+ }
2288
+ const payload = change.payload;
2289
+ const { version, change: updated } = await promoteChangeWithActiveCas(
2290
+ deps,
2291
+ grant.workspaceId,
2292
+ rig.id,
2293
+ change.id,
2294
+ {
2295
+ expectedActiveVersionId: change.baseVersionId,
2296
+ image: payload.image === void 0 ? base.image : payload.image,
2297
+ setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
2298
+ checks: Array.isArray(payload.checks) ? payload.checks : base.checks,
2299
+ credentialHooks: Array.isArray(payload.credentialHooks) ? payload.credentialHooks : base.credentialHooks,
2300
+ defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds) ? payload.defaultVariableSetIds : base.defaultVariableSetIds,
2301
+ changelog: typeof payload.changelog === "string" && payload.changelog.trim() ? payload.changelog : "Verified definition edit",
2302
+ createdBy: rigActorForGrant(grant)
2303
+ }
2304
+ );
2305
+ await recordRigAuditEvent(deps.db, {
2306
+ grant,
2307
+ action: "rig.change.merged",
2308
+ rigId: rig.id,
2309
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2310
+ });
2311
+ await recordRigAuditEvent(deps.db, {
2312
+ grant,
2313
+ action: "rig.version.promoted",
2314
+ rigId: rig.id,
2315
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2316
+ });
2317
+ return { change: updated, version };
2318
+ }
2319
+ async function createRigVersionForApi(deps, grant, rig, payload) {
2320
+ if (!rig.activeVersion) {
2321
+ throw new HTTPException7(422, { message: "rig has no active version" });
2322
+ }
2323
+ assertUniqueCheckNames(payload.checks);
2324
+ await assertVariableSetsExist(
2325
+ deps.db,
2326
+ grant.workspaceId,
2327
+ payload.defaultVariableSetIds ?? void 0
2328
+ );
2329
+ const base = rig.activeVersion;
2330
+ const version = await createRigVersion(
2331
+ deps.db,
2332
+ grant.workspaceId,
2333
+ rig.id,
2334
+ {
2335
+ image: payload.image === void 0 ? base.image : payload.image,
2336
+ setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
2337
+ checks: payload.checks ?? base.checks,
2338
+ credentialHooks: payload.credentialHooks ?? base.credentialHooks,
2339
+ defaultVariableSetIds: payload.defaultVariableSetIds ?? base.defaultVariableSetIds,
2340
+ changelog: payload.changelog ?? "Manager-created version",
2341
+ createdBy: rigActorForGrant(grant)
2342
+ },
2343
+ { activate: true }
2344
+ );
2345
+ await recordRigAuditEvent(deps.db, {
2346
+ grant,
2347
+ action: "rig.version.promoted",
2348
+ rigId: rig.id,
2349
+ metadata: { versionId: version.id, version: version.version, direct: true }
2350
+ });
2351
+ return version;
2352
+ }
2353
+ async function activateRigVersionForApi(deps, grant, rig, versionId) {
2354
+ const workspaceId = grant.workspaceId;
2355
+ const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
2356
+ await recordRigAuditEvent(deps.db, {
2357
+ grant,
2358
+ action: "rig.version.activated",
2359
+ rigId: rig.id,
2360
+ metadata: { versionId: version.id, version: version.version }
2361
+ });
2362
+ return version;
2363
+ }
2364
+ async function listRigVersionsForApi(deps, workspaceId, rigId) {
2365
+ return await listRigVersions(deps.db, workspaceId, rigId);
2366
+ }
2367
+ async function listRigChangesForApi(deps, workspaceId, rigId, limit) {
2368
+ return await listRigChanges(deps.db, workspaceId, rigId, limit);
2369
+ }
2370
+
1731
2371
  // src/domain/resources.ts
1732
2372
  import {
1733
2373
  mergeResourceRefs as mergeContractResourceRefs,
@@ -1736,26 +2376,27 @@ import {
1736
2376
  ResourceRefConflictError,
1737
2377
  stableJson
1738
2378
  } from "@opengeni/contracts";
1739
- import {
1740
- listGitHubInstallationIdsForWorkspace,
1741
- requireFile
1742
- } from "@opengeni/db";
1743
- import { HTTPException as HTTPException7 } from "hono/http-exception";
2379
+ import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
2380
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
1744
2381
  function validateToolRefs(tools, settings) {
1745
2382
  const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
1746
2383
  const out = [];
1747
2384
  for (const tool of tools) {
1748
2385
  if (tool.kind !== "mcp") {
1749
- throw new HTTPException7(422, { message: `unsupported tool kind: ${tool.kind}` });
2386
+ throw new HTTPException8(422, {
2387
+ message: `unsupported tool kind: ${tool.kind}`
2388
+ });
1750
2389
  }
1751
2390
  const optional = tool.optional === true;
1752
2391
  if (!mcpServerIds.has(tool.id)) {
1753
2392
  if (optional) {
1754
2393
  continue;
1755
2394
  }
1756
- throw new HTTPException7(422, { message: `unknown MCP server id: ${tool.id}` });
2395
+ throw new HTTPException8(422, { message: `unknown MCP server id: ${tool.id}` });
1757
2396
  }
1758
- out.push(optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id });
2397
+ out.push(
2398
+ optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id }
2399
+ );
1759
2400
  }
1760
2401
  return mergeToolRefs([], out);
1761
2402
  }
@@ -1783,12 +2424,12 @@ function normalizeResources(resources) {
1783
2424
  } else {
1784
2425
  const url = parseResourceUrl(resource.uri);
1785
2426
  if (url.protocol !== "https:" || !url.hostname) {
1786
- throw new HTTPException7(422, { message: "repository resources must use HTTPS Git URLs" });
2427
+ throw new HTTPException8(422, { message: "repository resources must use HTTPS Git URLs" });
1787
2428
  }
1788
2429
  const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
1789
2430
  const parts = path.split("/").filter(Boolean);
1790
2431
  if (parts.length < 2) {
1791
- throw new HTTPException7(422, { message: "repository URL must include owner and repo" });
2432
+ throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
1792
2433
  }
1793
2434
  const repo = parts.join("/");
1794
2435
  const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
@@ -1798,6 +2439,11 @@ function normalizeResources(resources) {
1798
2439
  ref: resource.ref.trim(),
1799
2440
  mountPath,
1800
2441
  ...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
2442
+ ...resource.provider ? { provider: resource.provider } : {},
2443
+ ...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
2444
+ ...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
2445
+ ...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
2446
+ ...resource.connectionId ? { connectionId: resource.connectionId } : {},
1801
2447
  ...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
1802
2448
  ...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
1803
2449
  };
@@ -1805,7 +2451,9 @@ function normalizeResources(resources) {
1805
2451
  const key = stableJson(normalized);
1806
2452
  const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
1807
2453
  if (mounted && mounted !== key) {
1808
- throw new HTTPException7(422, { message: `duplicate resource mount path: ${normalized.mountPath}` });
2454
+ throw new HTTPException8(422, {
2455
+ message: `duplicate resource mount path: ${normalized.mountPath}`
2456
+ });
1809
2457
  }
1810
2458
  if (normalized.mountPath) {
1811
2459
  mountPaths.set(normalized.mountPath, key);
@@ -1813,7 +2461,9 @@ function normalizeResources(resources) {
1813
2461
  const identity = resourceIdentityKey(normalized);
1814
2462
  const seenIdentity = identities.get(identity);
1815
2463
  if (seenIdentity && seenIdentity !== key) {
1816
- throw new HTTPException7(422, { message: `duplicate resource with different settings: ${identity}` });
2464
+ throw new HTTPException8(422, {
2465
+ message: `duplicate resource with different settings: ${identity}`
2466
+ });
1817
2467
  }
1818
2468
  identities.set(identity, key);
1819
2469
  if (!seenResources.has(key)) {
@@ -1828,7 +2478,7 @@ function mergeResourceRefs(existing, additions) {
1828
2478
  return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
1829
2479
  } catch (error) {
1830
2480
  if (error instanceof ResourceRefConflictError) {
1831
- throw new HTTPException7(422, { message: error.message });
2481
+ throw new HTTPException8(422, { message: error.message });
1832
2482
  }
1833
2483
  throw error;
1834
2484
  }
@@ -1838,8 +2488,8 @@ function validateGitHubRepositorySelectionShape(resources) {
1838
2488
  if (resource.kind !== "repository") {
1839
2489
  return [];
1840
2490
  }
1841
- const installationRaw = resource.githubInstallationId;
1842
- const repositoryRaw = resource.githubRepositoryId;
2491
+ const installationRaw = resource.githubInstallationId ?? (resource.provider === "github" ? resource.installationId : void 0);
2492
+ const repositoryRaw = resource.githubRepositoryId ?? (resource.provider === "github" ? resource.repositoryId : void 0);
1843
2493
  if (installationRaw === null && repositoryRaw === null) {
1844
2494
  return [];
1845
2495
  }
@@ -1849,7 +2499,7 @@ function validateGitHubRepositorySelectionShape(resources) {
1849
2499
  const installationId2 = positiveInteger(installationRaw);
1850
2500
  const repositoryId = positiveInteger(repositoryRaw);
1851
2501
  if (!installationId2 || !repositoryId) {
1852
- throw new HTTPException7(422, {
2502
+ throw new HTTPException8(422, {
1853
2503
  message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
1854
2504
  });
1855
2505
  }
@@ -1860,7 +2510,7 @@ function validateGitHubRepositorySelectionShape(resources) {
1860
2510
  }
1861
2511
  const installationId = selected[0].installationId;
1862
2512
  if (selected.some((item) => item.installationId !== installationId)) {
1863
- throw new HTTPException7(422, {
2513
+ throw new HTTPException8(422, {
1864
2514
  message: "GitHub App repository resources must belong to one installation"
1865
2515
  });
1866
2516
  }
@@ -1871,9 +2521,11 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
1871
2521
  if (installationId === null) {
1872
2522
  return;
1873
2523
  }
1874
- const linkedInstallationIds = new Set(await listGitHubInstallationIdsForWorkspace(db, workspaceId));
2524
+ const linkedInstallationIds = new Set(
2525
+ await listGitHubInstallationIdsForWorkspace(db, workspaceId)
2526
+ );
1875
2527
  if (!linkedInstallationIds.has(installationId)) {
1876
- throw new HTTPException7(422, {
2528
+ throw new HTTPException8(422, {
1877
2529
  message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
1878
2530
  });
1879
2531
  }
@@ -1885,22 +2537,24 @@ async function validateFileResources(db, workspaceId, resources) {
1885
2537
  continue;
1886
2538
  }
1887
2539
  if (fileIds.has(resource.fileId)) {
1888
- throw new HTTPException7(422, { message: `duplicate file resource: ${resource.fileId}` });
2540
+ throw new HTTPException8(422, { message: `duplicate file resource: ${resource.fileId}` });
1889
2541
  }
1890
2542
  fileIds.add(resource.fileId);
1891
2543
  const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
1892
2544
  if (!file) {
1893
- throw new HTTPException7(422, { message: `unknown file resource: ${resource.fileId}` });
2545
+ throw new HTTPException8(422, { message: `unknown file resource: ${resource.fileId}` });
1894
2546
  }
1895
2547
  if (file.status !== "ready") {
1896
- throw new HTTPException7(422, { message: `file resource ${resource.fileId} is ${file.status}` });
2548
+ throw new HTTPException8(422, {
2549
+ message: `file resource ${resource.fileId} is ${file.status}`
2550
+ });
1897
2551
  }
1898
2552
  }
1899
2553
  }
1900
2554
  function normalizeMountPath(path) {
1901
2555
  const normalized = path.trim().replace(/^\/+|\/+$/g, "");
1902
2556
  if (!normalized || normalized.includes("..")) {
1903
- throw new HTTPException7(422, { message: `invalid resource mount path: ${path}` });
2557
+ throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
1904
2558
  }
1905
2559
  return normalized;
1906
2560
  }
@@ -1908,7 +2562,7 @@ function parseResourceUrl(uri) {
1908
2562
  try {
1909
2563
  return new URL(uri);
1910
2564
  } catch {
1911
- throw new HTTPException7(422, { message: "repository resources must use valid URLs" });
2565
+ throw new HTTPException8(422, { message: "repository resources must use valid URLs" });
1912
2566
  }
1913
2567
  }
1914
2568
  function positiveInteger(value) {
@@ -1925,38 +2579,47 @@ function positiveInteger(value) {
1925
2579
  import {
1926
2580
  createScheduledTask,
1927
2581
  deleteScheduledTask,
2582
+ getRig as getRig3,
1928
2583
  getScheduledTask,
1929
2584
  updateScheduledTask
1930
2585
  } from "@opengeni/db";
1931
- import { HTTPException as HTTPException9 } from "hono/http-exception";
2586
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
1932
2587
 
1933
2588
  // src/domain/sessions.ts
1934
2589
  import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
1935
- import { configuredAllowedModels } from "@opengeni/config";
2590
+ import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
1936
2591
  import {
1937
2592
  CreateSessionRequest,
2593
+ evaluateWorkspaceModelPolicy,
1938
2594
  reasoningEffortForMetadata
1939
2595
  } from "@opengeni/contracts";
1940
2596
  import {
1941
- appendSessionEventsWithLockedSessionUpdate,
1942
2597
  createSession,
1943
- createSessionGoal,
1944
2598
  createSessionWithIdempotencyKey,
1945
- enqueueSessionTurn,
1946
- encryptEnvironmentValue as encryptEnvironmentValue2,
2599
+ enqueueSessionMessageAtomically,
2600
+ encryptVariableSetValue as encryptVariableSetValue2,
1947
2601
  getAnySessionInGroup,
1948
2602
  getEnrollment as getEnrollment2,
1949
- listDistinctEnvironmentIdsInGroup,
2603
+ getRig as getRig2,
2604
+ getWorkspaceDefaultRigId,
2605
+ listDistinctVariableSetIdsInGroup,
2606
+ listDistinctRigVersionIdsInGroup,
1950
2607
  getSandbox as getSandbox3,
1951
2608
  getSession,
1952
2609
  getSessionByCreateIdempotencyKey,
2610
+ getSessionLineage,
1953
2611
  getSessionTurn,
2612
+ getWorkspaceModelPolicy,
2613
+ initializeSessionStartAtomically,
1954
2614
  requireSession as requireSession2,
1955
- setTemporalWorkflowId,
1956
- updateSessionTitle as updateSessionTitleRow
2615
+ updateSessionTitle as updateSessionTitleRow,
2616
+ SessionQueueConflictError
1957
2617
  } from "@opengeni/db";
1958
- import { appendAndPublishEvents } from "@opengeni/events";
1959
- import { HTTPException as HTTPException8 } from "hono/http-exception";
2618
+ import {
2619
+ appendAndPublishEvents,
2620
+ publishDurableSessionEvents
2621
+ } from "@opengeni/events";
2622
+ import { HTTPException as HTTPException9 } from "hono/http-exception";
1960
2623
  var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
1961
2624
  var maxSessionMcpCredentialHeaders = 16;
1962
2625
  var maxSessionMcpCredentialHeaderValueLength = 4096;
@@ -1967,23 +2630,29 @@ function normalizedSessionMcpCredentialHeaders(headers) {
1967
2630
  }
1968
2631
  const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
1969
2632
  if (entries.length > maxSessionMcpCredentialHeaders) {
1970
- throw new HTTPException8(422, { message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers` });
2633
+ throw new HTTPException9(422, {
2634
+ message: `a session MCP server supports at most ${maxSessionMcpCredentialHeaders} credential headers`
2635
+ });
1971
2636
  }
1972
2637
  const seen = /* @__PURE__ */ new Set();
1973
2638
  for (const [name, value] of entries) {
1974
2639
  if (!sessionMcpCredentialHeaderName.test(name)) {
1975
- throw new HTTPException8(422, { message: `invalid credential header name: ${name}` });
2640
+ throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
1976
2641
  }
1977
2642
  const lower = name.toLowerCase();
1978
2643
  if (seen.has(lower)) {
1979
- throw new HTTPException8(422, { message: `duplicate credential header name: ${name}` });
2644
+ throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
1980
2645
  }
1981
2646
  seen.add(lower);
1982
2647
  if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
1983
- throw new HTTPException8(422, { message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters` });
2648
+ throw new HTTPException9(422, {
2649
+ message: `credential header ${name} must be 1-${maxSessionMcpCredentialHeaderValueLength} characters`
2650
+ });
1984
2651
  }
1985
2652
  if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
1986
- throw new HTTPException8(422, { message: `credential header ${name} contains forbidden control characters` });
2653
+ throw new HTTPException9(422, {
2654
+ message: `credential header ${name} contains forbidden control characters`
2655
+ });
1987
2656
  }
1988
2657
  }
1989
2658
  return Object.fromEntries(entries);
@@ -2014,10 +2683,7 @@ function settingsWithSessionMcpServerConfigs(settings, servers) {
2014
2683
  const sessionIds = new Set(servers.map((server) => server.id));
2015
2684
  return {
2016
2685
  ...settings,
2017
- mcpServers: [
2018
- ...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
2019
- ...servers
2020
- ]
2686
+ mcpServers: [...settings.mcpServers.filter((server) => !sessionIds.has(server.id)), ...servers]
2021
2687
  };
2022
2688
  }
2023
2689
  function settingsWithSessionMcpServerMetadata(settings, servers) {
@@ -2028,7 +2694,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2028
2694
  return { runtimeServers: [], dbServers: [], metadata: [] };
2029
2695
  }
2030
2696
  requirePermission(grant, "mcp_servers:attach");
2031
- const encryptionKey = requireEnvironmentEncryption(settings);
2697
+ const encryptionKey = requireVariableSetEncryption(settings);
2032
2698
  const existingIds = new Set(settings.mcpServers.map((server) => server.id));
2033
2699
  const seenIds = /* @__PURE__ */ new Set();
2034
2700
  const runtimeServers = [];
@@ -2036,15 +2702,18 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2036
2702
  const metadata = [];
2037
2703
  for (const server of servers) {
2038
2704
  if (seenIds.has(server.id)) {
2039
- throw new HTTPException8(422, { message: `duplicate session MCP server id: ${server.id}` });
2705
+ throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
2040
2706
  }
2041
2707
  seenIds.add(server.id);
2042
2708
  if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
2043
- throw new HTTPException8(422, { message: `MCP server id already exists: ${server.id}` });
2709
+ throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
2044
2710
  }
2045
2711
  const headers = normalizedSessionMcpCredentialHeaders(server.headers);
2046
2712
  const headersEncrypted = Object.fromEntries(
2047
- Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue2(encryptionKey, value)])
2713
+ Object.entries(headers).map(([name, value]) => [
2714
+ name,
2715
+ encryptVariableSetValue2(encryptionKey, value)
2716
+ ])
2048
2717
  );
2049
2718
  runtimeServers.push(mcpServerConfigFromInput(server));
2050
2719
  dbServers.push({
@@ -2072,22 +2741,27 @@ function validateSessionMcpCredentialUpdates(input) {
2072
2741
  return [];
2073
2742
  }
2074
2743
  requirePermission(input.grant, "mcp_servers:attach");
2075
- const encryptionKey = requireEnvironmentEncryption(input.settings);
2744
+ const encryptionKey = requireVariableSetEncryption(input.settings);
2076
2745
  const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
2077
2746
  const seenIds = /* @__PURE__ */ new Set();
2078
2747
  const encryptedUpdates = input.updates.map((update) => {
2079
2748
  if (seenIds.has(update.id)) {
2080
- throw new HTTPException8(422, { message: `duplicate session MCP credential update id: ${update.id}` });
2749
+ throw new HTTPException9(422, {
2750
+ message: `duplicate session MCP credential update id: ${update.id}`
2751
+ });
2081
2752
  }
2082
2753
  seenIds.add(update.id);
2083
2754
  if (!knownIds.has(update.id)) {
2084
- throw new HTTPException8(422, { message: `unknown session MCP server id: ${update.id}` });
2755
+ throw new HTTPException9(422, { message: `unknown session MCP server id: ${update.id}` });
2085
2756
  }
2086
2757
  const headers = normalizedSessionMcpCredentialHeaders(update.headers);
2087
2758
  return {
2088
2759
  id: update.id,
2089
2760
  headersEncrypted: Object.fromEntries(
2090
- Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue2(encryptionKey, value)])
2761
+ Object.entries(headers).map(([name, value]) => [
2762
+ name,
2763
+ encryptVariableSetValue2(encryptionKey, value)
2764
+ ])
2091
2765
  )
2092
2766
  };
2093
2767
  });
@@ -2100,9 +2774,16 @@ async function createAndStartSession(input) {
2100
2774
  reasoningEffort: input.reasoningEffort
2101
2775
  };
2102
2776
  if (input.createIdempotencyKey) {
2103
- const existing = await getSessionByCreateIdempotencyKey(input.db, input.workspaceId, input.createIdempotencyKey);
2777
+ const existing = await getSessionByCreateIdempotencyKey(
2778
+ input.db,
2779
+ input.workspaceId,
2780
+ input.createIdempotencyKey
2781
+ );
2104
2782
  if (existing) {
2105
- return existing;
2783
+ return await finishStartSession(
2784
+ existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2785
+ existing
2786
+ );
2106
2787
  }
2107
2788
  const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
2108
2789
  accountId: input.accountId,
@@ -2113,7 +2794,9 @@ async function createAndStartSession(input) {
2113
2794
  metadata: sessionMetadata,
2114
2795
  model: input.model,
2115
2796
  sandboxBackend: input.sandboxBackend,
2116
- environmentId: input.environment?.id ?? null,
2797
+ variableSetId: input.variableSet?.id ?? null,
2798
+ rigId: input.rigId ?? null,
2799
+ rigVersionId: input.rigVersionId ?? null,
2117
2800
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
2118
2801
  instructions: input.instructions ?? null,
2119
2802
  parentSessionId: input.parentSessionId ?? null,
@@ -2123,7 +2806,10 @@ async function createAndStartSession(input) {
2123
2806
  mcpServers: input.mcpServers ?? []
2124
2807
  });
2125
2808
  if (!created) {
2126
- return keyed;
2809
+ return await finishStartSession(
2810
+ keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2811
+ keyed
2812
+ );
2127
2813
  }
2128
2814
  return await finishStartSession(input, keyed);
2129
2815
  }
@@ -2136,7 +2822,9 @@ async function createAndStartSession(input) {
2136
2822
  metadata: sessionMetadata,
2137
2823
  model: input.model,
2138
2824
  sandboxBackend: input.sandboxBackend,
2139
- environmentId: input.environment?.id ?? null,
2825
+ variableSetId: input.variableSet?.id ?? null,
2826
+ rigId: input.rigId ?? null,
2827
+ rigVersionId: input.rigVersionId ?? null,
2140
2828
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
2141
2829
  instructions: input.instructions ?? null,
2142
2830
  parentSessionId: input.parentSessionId ?? null,
@@ -2147,54 +2835,9 @@ async function createAndStartSession(input) {
2147
2835
  return await finishStartSession(input, session);
2148
2836
  }
2149
2837
  async function finishStartSession(input, session) {
2150
- const goal = input.goal ? await createSessionGoal(input.db, {
2151
- accountId: session.accountId,
2152
- workspaceId: session.workspaceId,
2153
- sessionId: session.id,
2154
- text: input.goal.text,
2155
- successCriteria: input.goal.successCriteria ?? null,
2156
- maxAutoContinuations: input.goal.maxAutoContinuations ?? null,
2157
- createdBy: "api"
2158
- }) : null;
2159
- const initialPayload = {
2160
- text: input.initialMessage,
2161
- ...input.resources.length ? { resources: input.resources } : {},
2162
- ...input.tools.length ? { tools: input.tools } : {}
2163
- };
2164
- const events = await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [
2165
- {
2166
- type: "session.created",
2167
- payload: {
2168
- status: "queued",
2169
- ...input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {},
2170
- ...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
2171
- }
2172
- },
2173
- ...goal ? [{
2174
- type: "goal.set",
2175
- payload: {
2176
- goalId: goal.id,
2177
- text: goal.text,
2178
- ...goal.successCriteria ? { successCriteria: goal.successCriteria } : {},
2179
- version: goal.version,
2180
- actor: "api",
2181
- replaced: false
2182
- }
2183
- }] : [],
2184
- {
2185
- type: "user.message",
2186
- payload: initialPayload,
2187
- ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2188
- },
2189
- { type: "session.status.changed", payload: { status: "queued" } }
2190
- ]);
2191
- const userEvent = events.find((event) => event.type === "user.message");
2192
- if (!userEvent) {
2193
- throw new HTTPException8(500, { message: "failed to append initial user event" });
2194
- }
2195
2838
  if (input.seedTargetSandbox) {
2196
2839
  if (session.sandboxBackend === "none") {
2197
- throw new HTTPException8(422, {
2840
+ throw new HTTPException9(422, {
2198
2841
  message: "cannot target a machine for a session with no sandbox (backend: none)"
2199
2842
  });
2200
2843
  }
@@ -2214,34 +2857,37 @@ async function finishStartSession(input, session) {
2214
2857
  input.seedTargetSandbox.workingDir ?? null
2215
2858
  );
2216
2859
  if (!seeded.swapped) {
2217
- throw new HTTPException8(422, {
2860
+ throw new HTTPException9(422, {
2218
2861
  message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
2219
2862
  });
2220
2863
  }
2221
2864
  }
2222
- const workflowId = workflowIdForSession(session.id);
2223
- await setTemporalWorkflowId(input.db, session.workspaceId, session.id, workflowId);
2224
- const turn = await enqueueSessionTurn(input.db, {
2865
+ const started = await initializeSessionStartAtomically(input.db, {
2225
2866
  accountId: session.accountId,
2226
2867
  workspaceId: session.workspaceId,
2227
2868
  sessionId: session.id,
2228
- triggerEventId: userEvent.id,
2229
- temporalWorkflowId: workflowId,
2230
- source: "user",
2231
- prompt: input.initialMessage,
2232
- resources: input.resources,
2233
- tools: input.tools,
2234
- model: input.model,
2235
- reasoningEffort: input.reasoningEffort,
2236
- sandboxBackend: input.sandboxBackend,
2237
- metadata: {}
2869
+ ...input.clientEventId ? { clientEventId: input.clientEventId } : {},
2870
+ reasoningEffortFallback: input.reasoningEffort,
2871
+ createdEventPayload: {
2872
+ ...input.variableSet ? { variableSetId: input.variableSet.id, variableSetName: input.variableSet.name } : {},
2873
+ ...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
2874
+ },
2875
+ goal: input.goal ? {
2876
+ text: input.goal.text,
2877
+ ...input.goal.successCriteria !== void 0 ? { successCriteria: input.goal.successCriteria } : {},
2878
+ ...input.goal.maxAutoContinuations !== void 0 ? { maxAutoContinuations: input.goal.maxAutoContinuations } : {}
2879
+ } : null
2238
2880
  });
2239
- await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [{
2240
- type: "turn.queued",
2241
- turnId: turn.id,
2242
- payload: { turnId: turn.id, triggerEventId: userEvent.id, source: turn.source }
2243
- }]);
2244
- await input.workflowClient.wakeSessionWorkflow({ accountId: session.accountId, workspaceId: session.workspaceId, sessionId: session.id, workflowId });
2881
+ await publishDurableSessionEvents(input.bus, session.workspaceId, session.id, started.events);
2882
+ if (started.workflowWakeRevision !== null) {
2883
+ await input.workflowClient.wakeSessionWorkflow({
2884
+ accountId: session.accountId,
2885
+ workspaceId: session.workspaceId,
2886
+ sessionId: session.id,
2887
+ workflowId: started.temporalWorkflowId,
2888
+ wakeRevision: started.workflowWakeRevision
2889
+ });
2890
+ }
2245
2891
  return await requireSession2(input.db, session.workspaceId, session.id);
2246
2892
  }
2247
2893
  function workflowIdForSession(sessionId) {
@@ -2257,15 +2903,33 @@ function assertConfiguredModel(settings, model) {
2257
2903
  if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
2258
2904
  return;
2259
2905
  }
2260
- throw new HTTPException8(422, { message: `model is not available: ${model}` });
2906
+ throw new HTTPException9(422, { message: `model is not available: ${model}` });
2907
+ }
2908
+ async function assertWorkspaceModelPolicyAllows(db, settings, workspaceId, model) {
2909
+ if (model === null || model === void 0) {
2910
+ return;
2911
+ }
2912
+ const policy = await getWorkspaceModelPolicy(db, workspaceId);
2913
+ if (!policy) {
2914
+ return;
2915
+ }
2916
+ const providerId = policyProviderIdForModel(settings, model);
2917
+ const verdict = evaluateWorkspaceModelPolicy(policy, { providerId, modelId: model });
2918
+ if (!verdict.allowed) {
2919
+ throw new HTTPException9(422, {
2920
+ message: verdict.reason === "provider" ? `model "${model}" is not allowed by this workspace's model policy: provider "${providerId}" is not in the allowed providers` : `model "${model}" is not allowed by this workspace's model policy`
2921
+ });
2922
+ }
2261
2923
  }
2262
2924
  async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
2263
2925
  const turn = await getSessionTurn(db, workspaceId, turnId);
2264
2926
  if (!turn || turn.sessionId !== sessionId) {
2265
- throw new HTTPException8(404, { message: "session turn not found" });
2927
+ throw new HTTPException9(404, { message: "session turn not found" });
2266
2928
  }
2267
2929
  if (turn.status !== "queued") {
2268
- throw new HTTPException8(409, { message: `turn is ${turn.status}; only queued turns can be changed` });
2930
+ throw new HTTPException9(409, {
2931
+ message: `turn is ${turn.status}; only queued turns can be changed`
2932
+ });
2269
2933
  }
2270
2934
  return turn;
2271
2935
  }
@@ -2277,98 +2941,139 @@ async function postUserMessageTurn(input) {
2277
2941
  const requestedModel = input.model ?? null;
2278
2942
  const requestedReasoningEffort = input.reasoningEffort ?? null;
2279
2943
  assertConfiguredModel(settings, requestedModel);
2280
- const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, async (lockedSession, lockedUpdate) => {
2281
- if (lockedSession.status === "cancelled") {
2282
- throw new HTTPException8(409, { message: `session is ${lockedSession.status}; cannot accept a new user message` });
2944
+ await assertWorkspaceModelPolicyAllows(db, settings, workspaceId, requestedModel);
2945
+ let result;
2946
+ try {
2947
+ result = await enqueueSessionMessageAtomically(db, {
2948
+ accountId,
2949
+ workspaceId,
2950
+ sessionId,
2951
+ actor: input.actor ?? accountId,
2952
+ origin: input.origin ?? "human",
2953
+ text: input.text,
2954
+ resources: input.resources,
2955
+ tools: input.tools,
2956
+ model: requestedModel,
2957
+ reasoningEffort: requestedReasoningEffort,
2958
+ clientEventId: input.clientEventId ?? null,
2959
+ mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
2960
+ delivery: input.delivery ?? "queue",
2961
+ ...input.expectedControlGeneration !== void 0 ? { expectedControlGeneration: input.expectedControlGeneration } : {},
2962
+ ...input.expectedWorkspaceInferenceGeneration !== void 0 ? {
2963
+ expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration
2964
+ } : {},
2965
+ reasoningEffortFallback: settings.openaiReasoningEffort
2966
+ });
2967
+ } catch (error) {
2968
+ if (error instanceof SessionQueueConflictError) {
2969
+ throw new HTTPException9(409, { message: error.message });
2283
2970
  }
2284
- const mcpCredentialUpdates = input.mcpCredentialUpdates?.length ? await lockedUpdate.updateSessionMcpServerCredentials(input.mcpCredentialUpdates) : { servers: [], missingIds: [] };
2285
- if (mcpCredentialUpdates.missingIds.length > 0) {
2286
- throw new HTTPException8(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
2971
+ if (error instanceof Error && error.message.includes("cancelled")) {
2972
+ throw new HTTPException9(409, { message: error.message });
2287
2973
  }
2288
- const nextResources = mergeResourceRefs(lockedSession.resources, input.resources);
2289
- const nextTools = mergeToolRefs(lockedSession.tools, input.tools);
2290
- const shouldQueueSession = lockedSession.status === "idle" || lockedSession.status === "failed";
2291
- return {
2292
- events: [
2293
- {
2294
- type: "user.message",
2295
- payload: {
2296
- text: input.text,
2297
- ...input.resources.length ? { resources: input.resources } : {},
2298
- ...input.tools.length ? { tools: input.tools } : {},
2299
- ...requestedModel ? { model: requestedModel } : {},
2300
- ...requestedReasoningEffort ? { reasoningEffort: requestedReasoningEffort } : {},
2301
- ...mcpCredentialUpdates.servers.length ? { mcpCredentialUpdates: mcpCredentialUpdates.servers } : {}
2302
- },
2303
- ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2304
- },
2305
- ...shouldQueueSession ? [{ type: "session.status.changed", payload: { status: "queued" } }] : []
2306
- ],
2307
- update: {
2308
- resources: nextResources,
2309
- tools: nextTools,
2310
- ...shouldQueueSession ? { status: "queued", activeTurnId: null } : {}
2311
- }
2312
- };
2313
- }).then(async (events) => {
2314
- await bus.publish(workspaceId, sessionId, events);
2315
- return events;
2316
- });
2317
- const accepted = appended[0];
2318
- if (!accepted) {
2319
- throw new HTTPException8(500, { message: "failed to append client event" });
2320
- }
2321
- const workflowId = workflowIdForSession(sessionId);
2322
- const session = await requireSession2(db, workspaceId, sessionId);
2323
- const turn = await enqueueSessionTurn(db, {
2324
- accountId,
2325
- workspaceId,
2326
- sessionId,
2327
- triggerEventId: accepted.id,
2328
- temporalWorkflowId: workflowId,
2329
- source: "user",
2330
- prompt: input.text,
2331
- resources: input.resources,
2332
- tools: input.tools,
2333
- model: requestedModel ?? session.model,
2334
- reasoningEffort: requestedReasoningEffort ?? reasoningEffortForSession(session.metadata, settings.openaiReasoningEffort),
2335
- sandboxBackend: session.sandboxBackend,
2336
- metadata: {}
2337
- });
2338
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
2339
- type: "turn.queued",
2340
- turnId: turn.id,
2341
- payload: { turnId: turn.id, triggerEventId: accepted.id, source: turn.source }
2342
- }]);
2343
- await workflowClient.wakeSessionWorkflow({ accountId, workspaceId, sessionId, workflowId });
2344
- return { accepted, turn };
2974
+ if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
2975
+ throw new HTTPException9(422, { message: error.message });
2976
+ }
2977
+ throw error;
2978
+ }
2979
+ await bus.publish(workspaceId, sessionId, result.events);
2980
+ if (result.shouldSignalControl && result.controlEvent) {
2981
+ if (result.workflowWakeRevision === null) {
2982
+ throw new Error("Steer control has no workflow wake revision");
2983
+ }
2984
+ await workflowClient.signalSessionControl({
2985
+ accountId,
2986
+ workspaceId,
2987
+ sessionId,
2988
+ eventId: result.controlEvent.id,
2989
+ workflowId: result.temporalWorkflowId,
2990
+ workflowWakeRevision: result.workflowWakeRevision
2991
+ });
2992
+ } else if (result.shouldWake) {
2993
+ if (result.workflowWakeRevision === null) {
2994
+ throw new Error("Runnable prompt has no workflow wake revision");
2995
+ }
2996
+ await workflowClient.wakeSessionWorkflow({
2997
+ accountId,
2998
+ workspaceId,
2999
+ sessionId,
3000
+ workflowId: result.temporalWorkflowId,
3001
+ wakeRevision: result.workflowWakeRevision
3002
+ });
3003
+ }
3004
+ return {
3005
+ accepted: result.accepted,
3006
+ turn: result.turn
3007
+ };
2345
3008
  }
2346
3009
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2347
3010
  const { settings, db, bus, workflowClient, objectStorage } = deps;
2348
3011
  const payload = CreateSessionRequest.parse(rawPayload);
2349
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
2350
- const sessionMcpServers = validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
2351
- const runtimeSettings = settingsWithSessionMcpServerConfigs(capabilityRuntimeSettings, sessionMcpServers.runtimeServers);
3012
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3013
+ db,
3014
+ workspaceId,
3015
+ settings
3016
+ );
3017
+ const sessionMcpServers = validateSessionMcpServersForCreate(
3018
+ capabilityRuntimeSettings,
3019
+ grant,
3020
+ payload.mcpServers
3021
+ );
3022
+ const runtimeSettings = settingsWithSessionMcpServerConfigs(
3023
+ capabilityRuntimeSettings,
3024
+ sessionMcpServers.runtimeServers
3025
+ );
2352
3026
  const resources = normalizeResources(payload.resources);
2353
3027
  const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
2354
3028
  const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
2355
3029
  const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
2356
3030
  await validateGitHubRepositorySelection(db, workspaceId, resources);
2357
3031
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
2358
- throw new HTTPException8(503, { message: "object storage is not configured" });
3032
+ throw new HTTPException9(503, { message: "object storage is not configured" });
2359
3033
  }
2360
3034
  await validateFileResources(db, workspaceId, resources);
2361
- const environment = payload.environmentId ? await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, payload.environmentId) : null;
3035
+ const variableSet = payload.variableSetId ? await validateVariableSetAttachment(
3036
+ { settings, db },
3037
+ grant,
3038
+ workspaceId,
3039
+ payload.variableSetId
3040
+ ) : null;
3041
+ const requestedRigId = payload.rigId ?? await getWorkspaceDefaultRigId(db, workspaceId);
3042
+ let frozenRigId = null;
3043
+ let frozenRigVersionId = null;
3044
+ if (requestedRigId) {
3045
+ const rig = await getRig2(db, workspaceId, requestedRigId);
3046
+ if (!rig || !rig.activeVersion) {
3047
+ if (payload.rigId) {
3048
+ throw new HTTPException9(422, {
3049
+ message: rig ? `rig ${payload.rigId} has no active version to bind` : `unknown rigId: ${payload.rigId}`
3050
+ });
3051
+ }
3052
+ } else {
3053
+ frozenRigId = rig.id;
3054
+ frozenRigVersionId = rig.activeVersion.id;
3055
+ }
3056
+ }
2362
3057
  assertConfiguredModel(settings, payload.model);
3058
+ await assertWorkspaceModelPolicyAllows(
3059
+ db,
3060
+ settings,
3061
+ workspaceId,
3062
+ payload.model ?? settings.openaiModel
3063
+ );
2363
3064
  const model = payload.model ?? settings.openaiModel;
2364
3065
  const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
2365
3066
  let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
2366
3067
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
2367
- throw new HTTPException8(422, { message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set" });
3068
+ throw new HTTPException9(422, {
3069
+ message: "firstPartyMcpPermissions must not be empty; omit it for the default worker permission set"
3070
+ });
2368
3071
  }
2369
3072
  for (const permission of firstPartyMcpPermissions ?? []) {
2370
3073
  if (!hasPermission(grant.permissions, permission)) {
2371
- throw new HTTPException8(403, { message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}` });
3074
+ throw new HTTPException9(403, {
3075
+ message: `cannot grant first-party MCP permission beyond the creating grant: ${permission}`
3076
+ });
2372
3077
  }
2373
3078
  }
2374
3079
  if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
@@ -2378,19 +3083,39 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2378
3083
  const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
2379
3084
  let sandboxGroupId = null;
2380
3085
  let inheritedBackend;
2381
- const requestedEnvironmentId = payload.environmentId ?? null;
2382
- const environmentMatchesGroup = (memberEnvironmentId) => memberEnvironmentId === requestedEnvironmentId;
3086
+ const requestedVariableSetId = payload.variableSetId ?? null;
3087
+ const variableSetMatchesGroup = (memberVariableSetId) => memberVariableSetId === requestedVariableSetId;
3088
+ const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
2383
3089
  if (sandboxChoice === "shared") {
2384
3090
  if (!parentSessionId) {
2385
- throw new HTTPException8(422, { message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create." });
3091
+ throw new HTTPException9(422, {
3092
+ message: "sandbox:'shared' requires a parent session (spawn from inside a session); use 'new' for a top-level create."
3093
+ });
2386
3094
  }
2387
3095
  const parent = await getSession(db, workspaceId, parentSessionId);
2388
3096
  if (!parent) {
2389
- throw new HTTPException8(404, { message: `parent session not found in workspace: ${parentSessionId}` });
3097
+ throw new HTTPException9(404, {
3098
+ message: `parent session not found in workspace: ${parentSessionId}`
3099
+ });
2390
3100
  }
2391
- if (parent.sandboxBackend !== "none" && !environmentMatchesGroup(parent.environmentId ?? null)) {
3101
+ const parentBoxed = parent.sandboxBackend !== "none";
3102
+ const variableSetMismatch = parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
3103
+ let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
3104
+ if (parentBoxed && !rigMismatch) {
3105
+ const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
3106
+ db,
3107
+ workspaceId,
3108
+ parent.sandboxGroupId
3109
+ );
3110
+ rigMismatch = !memberRigVersionIds.every(
3111
+ (memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
3112
+ );
3113
+ }
3114
+ if (variableSetMismatch || rigMismatch) {
2392
3115
  if (payload.sandbox === "shared") {
2393
- throw new HTTPException8(422, { message: "sandbox:'shared' requires the same environment as the creator's box (the box environment is fixed at creation); omit sandbox or pass 'new' when attaching a different environment." });
3116
+ throw new HTTPException9(422, {
3117
+ message: variableSetMismatch ? "sandbox:'shared' requires the same variableSet / same environment as the creator's box (the box variable set/environment is fixed at creation); omit sandbox or pass 'new' when attaching a different variableSet/environment." : "sandbox:'shared' requires the same rig as the creator's box (the box's rig setup is fixed at creation); omit sandbox or pass 'new' when binding a different rig."
3118
+ });
2394
3119
  }
2395
3120
  } else {
2396
3121
  sandboxGroupId = parent.sandboxGroupId;
@@ -2399,19 +3124,43 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2399
3124
  } else if (typeof sandboxChoice === "object") {
2400
3125
  const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
2401
3126
  if (!member) {
2402
- throw new HTTPException8(404, { message: `sandbox group not found in workspace: ${sandboxChoice.groupId}` });
3127
+ throw new HTTPException9(404, {
3128
+ message: `sandbox group not found in workspace: ${sandboxChoice.groupId}`
3129
+ });
2403
3130
  }
2404
3131
  if (member.sandboxBackend !== "none") {
2405
- const memberEnvironmentIds = await listDistinctEnvironmentIdsInGroup(db, workspaceId, sandboxChoice.groupId);
2406
- if (!memberEnvironmentIds.every((memberEnvironmentId) => environmentMatchesGroup(memberEnvironmentId))) {
2407
- throw new HTTPException8(422, { message: `sandbox group ${sandboxChoice.groupId} runs a different environment (the box environment is fixed at creation); create with the group's environment or omit sandbox for an own box.` });
3132
+ const memberVariableSetIds = await listDistinctVariableSetIdsInGroup(
3133
+ db,
3134
+ workspaceId,
3135
+ sandboxChoice.groupId
3136
+ );
3137
+ if (!memberVariableSetIds.every(
3138
+ (memberVariableSetId) => variableSetMatchesGroup(memberVariableSetId)
3139
+ )) {
3140
+ throw new HTTPException9(422, {
3141
+ message: `sandbox group ${sandboxChoice.groupId} runs a different variableSet / different environment (the box variable set/environment is fixed at creation); create with the group's variableSet/environment or omit sandbox for an own box.`
3142
+ });
3143
+ }
3144
+ const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
3145
+ db,
3146
+ workspaceId,
3147
+ sandboxChoice.groupId
3148
+ );
3149
+ if (!memberRigVersionIds.every(
3150
+ (memberRigVersionId) => rigVersionMatchesGroup(memberRigVersionId)
3151
+ )) {
3152
+ throw new HTTPException9(422, {
3153
+ message: `sandbox group ${sandboxChoice.groupId} runs a different rig (the box's rig setup is fixed at creation); create with the group's rig or omit sandbox for an own box.`
3154
+ });
2408
3155
  }
2409
3156
  }
2410
3157
  sandboxGroupId = sandboxChoice.groupId;
2411
3158
  inheritedBackend = member.sandboxBackend;
2412
3159
  }
2413
3160
  if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
2414
- throw new HTTPException8(422, { message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)" });
3161
+ throw new HTTPException9(422, {
3162
+ message: "workingDir requires targetSandboxId (it is the targeted machine's working directory)"
3163
+ });
2415
3164
  }
2416
3165
  let machineHomeBackend;
2417
3166
  let machineHomeOs;
@@ -2427,7 +3176,13 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2427
3176
  }
2428
3177
  }
2429
3178
  }
2430
- await requireLimit(deps, { accountId: grant.accountId, workspaceId, action: "agent_run:create", quantity: 1, model });
3179
+ await requireLimit(deps, {
3180
+ accountId: grant.accountId,
3181
+ workspaceId,
3182
+ action: "agent_run:create",
3183
+ quantity: 1,
3184
+ model
3185
+ });
2431
3186
  const session = await createAndStartSession({
2432
3187
  db,
2433
3188
  bus,
@@ -2452,7 +3207,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2452
3207
  ...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
2453
3208
  sandboxGroupId,
2454
3209
  metadata: payload.metadata,
2455
- environment: environment ? { id: environment.id, name: environment.name } : null,
3210
+ variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
3211
+ // Frozen rig binding (M3): both null for a rig-less session (today's path).
3212
+ rigId: frozenRigId,
3213
+ rigVersionId: frozenRigVersionId,
2456
3214
  goal: payload.goal ?? null,
2457
3215
  // Per-session persona instructions (already trimmed/validated by the
2458
3216
  // contracts schema). Persisted on the row; composed system-level at turn
@@ -2484,9 +3242,16 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2484
3242
  }
2485
3243
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
2486
3244
  const { settings, db, bus, workflowClient, objectStorage } = deps;
2487
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3245
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3246
+ db,
3247
+ workspaceId,
3248
+ settings
3249
+ );
2488
3250
  const existingSession = await requireSession2(db, workspaceId, sessionId);
2489
- const runtimeSettings = settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, existingSession.mcpServers);
3251
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
3252
+ capabilityRuntimeSettings,
3253
+ existingSession.mcpServers
3254
+ );
2490
3255
  const requestedResources = normalizeResources(input.resources ?? []);
2491
3256
  const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
2492
3257
  const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
@@ -2498,10 +3263,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
2498
3263
  model: input.model ?? existingSession.model
2499
3264
  });
2500
3265
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
2501
- throw new HTTPException8(503, { message: "object storage is not configured" });
3266
+ throw new HTTPException9(503, { message: "object storage is not configured" });
2502
3267
  }
2503
3268
  await validateFileResources(db, workspaceId, requestedResources);
2504
- await validateGitHubRepositorySelection(db, workspaceId, [...existingSession.resources, ...requestedResources]);
3269
+ await validateGitHubRepositorySelection(db, workspaceId, [
3270
+ ...existingSession.resources,
3271
+ ...requestedResources
3272
+ ]);
2505
3273
  const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
2506
3274
  settings,
2507
3275
  grant,
@@ -2522,6 +3290,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
2522
3290
  model: input.model ?? null,
2523
3291
  reasoningEffort: input.reasoningEffort ?? null,
2524
3292
  mcpCredentialUpdates,
3293
+ delivery: input.delivery ?? "queue",
3294
+ origin: input.origin ?? "human",
3295
+ actor: grant.subjectId,
3296
+ ...input.expectedControlGeneration !== void 0 ? { expectedControlGeneration: input.expectedControlGeneration } : {},
3297
+ ...input.expectedWorkspaceInferenceGeneration !== void 0 ? {
3298
+ expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration
3299
+ } : {},
2525
3300
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2526
3301
  });
2527
3302
  await recordWorkspaceUsage(deps, {
@@ -2541,16 +3316,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
2541
3316
  const { db, bus } = deps;
2542
3317
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
2543
3318
  if (result.updated) {
2544
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
2545
- type: "session.title_set",
2546
- payload: {
2547
- title: result.title ?? title,
2548
- source
3319
+ await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
3320
+ {
3321
+ type: "session.title_set",
3322
+ payload: {
3323
+ title: result.title ?? title,
3324
+ source
3325
+ }
2549
3326
  }
2550
- }]);
3327
+ ]);
2551
3328
  }
2552
3329
  return result;
2553
3330
  }
3331
+ async function readSessionLineage(db, workspaceId, sessionId) {
3332
+ const lineage = await getSessionLineage(db, workspaceId, sessionId);
3333
+ if (!lineage) {
3334
+ throw new HTTPException9(404, { message: "session not found" });
3335
+ }
3336
+ return lineage;
3337
+ }
2554
3338
  function withFirstPartyTools(tools, runtimeSettings) {
2555
3339
  if (!runtimeSettings.mcpServers.some((server) => server.id === "opengeni")) {
2556
3340
  return tools;
@@ -2558,7 +3342,9 @@ function withFirstPartyTools(tools, runtimeSettings) {
2558
3342
  return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
2559
3343
  }
2560
3344
  function hasOwnProperty(value, key) {
2561
- return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
3345
+ return Boolean(
3346
+ value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key)
3347
+ );
2562
3348
  }
2563
3349
 
2564
3350
  // src/domain/scheduled-tasks.ts
@@ -2572,18 +3358,24 @@ function scheduledTaskToolsProvided(rawPayload) {
2572
3358
  );
2573
3359
  }
2574
3360
  async function createValidatedScheduledTask(input) {
2575
- const agentConfig = await validateScheduledTaskAgentConfig({ ...input, workspaceId: input.grant.workspaceId });
3361
+ const agentConfig = await validateScheduledTaskAgentConfig({
3362
+ ...input,
3363
+ workspaceId: input.grant.workspaceId
3364
+ });
2576
3365
  const id = crypto.randomUUID();
2577
3366
  validateScheduledTaskSchedule(input.payload.schedule);
2578
- if (input.payload.environmentId) {
2579
- await validateEnvironmentAttachment(
3367
+ if (input.payload.variableSetId) {
3368
+ await validateVariableSetAttachment(
2580
3369
  { settings: input.settings, db: input.db },
2581
3370
  input.grant,
2582
3371
  input.grant.workspaceId,
2583
- input.payload.environmentId,
2584
- { preauthorized: input.environmentPreauthorized ?? false }
3372
+ input.payload.variableSetId,
3373
+ { preauthorized: input.variableSetPreauthorized ?? false }
2585
3374
  );
2586
3375
  }
3376
+ if (input.payload.rigId) {
3377
+ await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
3378
+ }
2587
3379
  return await createScheduledTask(input.db, {
2588
3380
  id,
2589
3381
  accountId: input.grant.accountId,
@@ -2595,10 +3387,17 @@ async function createValidatedScheduledTask(input) {
2595
3387
  runMode: input.payload.runMode,
2596
3388
  overlapPolicy: input.payload.overlapPolicy,
2597
3389
  agentConfig,
2598
- environmentId: input.payload.environmentId ?? null,
3390
+ variableSetId: input.payload.variableSetId ?? null,
3391
+ rigId: input.payload.rigId ?? null,
2599
3392
  metadata: input.payload.metadata
2600
3393
  });
2601
3394
  }
3395
+ async function requireScheduledTaskRig(db, workspaceId, rigId) {
3396
+ const rig = await getRig3(db, workspaceId, rigId);
3397
+ if (!rig) {
3398
+ throw new HTTPException10(422, { message: `unknown rigId: ${rigId}` });
3399
+ }
3400
+ }
2602
3401
  async function validatedScheduledTaskUpdate(input) {
2603
3402
  const update = {};
2604
3403
  if (input.payload.name !== void 0) {
@@ -2620,30 +3419,38 @@ async function validatedScheduledTaskUpdate(input) {
2620
3419
  if (input.payload.metadata !== void 0) {
2621
3420
  update.metadata = input.payload.metadata;
2622
3421
  }
2623
- if (input.payload.environmentId !== void 0) {
2624
- const nextEnvironmentId = input.payload.environmentId;
2625
- if ((input.existing.environmentId ?? null) !== (nextEnvironmentId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
2626
- throw new HTTPException9(409, { message: "cannot change environment of a task with a live reusable session; recreate the task" });
3422
+ if (input.payload.variableSetId !== void 0) {
3423
+ const nextVariableSetId = input.payload.variableSetId;
3424
+ if ((input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
3425
+ throw new HTTPException10(409, {
3426
+ message: "cannot change variableSet of a task with a live reusable session; recreate the task"
3427
+ });
2627
3428
  }
2628
- if (nextEnvironmentId === null) {
2629
- if (input.existing.environmentId !== null) {
2630
- requirePermission(input.grant, "environments:use");
3429
+ if (nextVariableSetId === null) {
3430
+ if (input.existing.variableSetId !== null) {
3431
+ requirePermission(input.grant, "variable-sets:use");
2631
3432
  }
2632
- update.environmentId = null;
3433
+ update.variableSetId = null;
2633
3434
  } else {
2634
- await validateEnvironmentAttachment(
3435
+ await validateVariableSetAttachment(
2635
3436
  { settings: input.settings, db: input.db },
2636
3437
  input.grant,
2637
3438
  input.existing.workspaceId,
2638
- nextEnvironmentId
3439
+ nextVariableSetId
2639
3440
  );
2640
- update.environmentId = nextEnvironmentId;
3441
+ update.variableSetId = nextVariableSetId;
2641
3442
  }
2642
3443
  }
3444
+ if (input.payload.rigId !== void 0) {
3445
+ if (input.payload.rigId !== null) {
3446
+ await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
3447
+ }
3448
+ update.rigId = input.payload.rigId;
3449
+ }
2643
3450
  if (input.payload.agentConfig !== void 0) {
2644
- const willHaveEnvironment = input.payload.environmentId !== void 0 ? input.payload.environmentId !== null : Boolean(input.existing.environmentId);
2645
- if (willHaveEnvironment) {
2646
- requirePermission(input.grant, "environments:use");
3451
+ const willHaveVariableSet = input.payload.variableSetId !== void 0 ? input.payload.variableSetId !== null : Boolean(input.existing.variableSetId);
3452
+ if (willHaveVariableSet) {
3453
+ requirePermission(input.grant, "variable-sets:use");
2647
3454
  }
2648
3455
  update.agentConfig = await validateScheduledTaskAgentConfig({
2649
3456
  settings: input.settings,
@@ -2659,7 +3466,7 @@ async function validatedScheduledTaskUpdate(input) {
2659
3466
  async function requireScheduledTaskForApi(db, workspaceId, taskId) {
2660
3467
  const task = await getScheduledTask(db, workspaceId, taskId);
2661
3468
  if (!task) {
2662
- throw new HTTPException9(404, { message: "scheduled task not found" });
3469
+ throw new HTTPException10(404, { message: "scheduled task not found" });
2663
3470
  }
2664
3471
  return task;
2665
3472
  }
@@ -2672,7 +3479,7 @@ async function restoreScheduledTask(db, task) {
2672
3479
  overlapPolicy: task.overlapPolicy,
2673
3480
  agentConfig: task.agentConfig,
2674
3481
  reusableSessionId: task.reusableSessionId,
2675
- environmentId: task.environmentId,
3482
+ variableSetId: task.variableSetId,
2676
3483
  metadata: task.metadata
2677
3484
  });
2678
3485
  }
@@ -2680,7 +3487,9 @@ async function syncCreatedScheduledTask(input) {
2680
3487
  try {
2681
3488
  await input.workflowClient.syncScheduledTask({ task: input.task });
2682
3489
  } catch (error) {
2683
- await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(() => void 0);
3490
+ await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
3491
+ () => void 0
3492
+ );
2684
3493
  throw error;
2685
3494
  }
2686
3495
  }
@@ -2711,17 +3520,27 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
2711
3520
  }
2712
3521
  async function validateScheduledTaskAgentConfig(input) {
2713
3522
  assertConfiguredModel(input.settings, input.payload.agentConfig.model);
3523
+ await assertWorkspaceModelPolicyAllows(
3524
+ input.db,
3525
+ input.settings,
3526
+ input.workspaceId,
3527
+ input.payload.agentConfig.model
3528
+ );
2714
3529
  const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
2715
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(input.db, input.workspaceId, input.settings);
3530
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3531
+ input.db,
3532
+ input.workspaceId,
3533
+ input.settings
3534
+ );
2716
3535
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
2717
3536
  const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
2718
3537
  const prompt = input.payload.agentConfig.prompt.trim();
2719
3538
  if (!prompt) {
2720
- throw new HTTPException9(422, { message: "scheduled task prompt is required" });
3539
+ throw new HTTPException10(422, { message: "scheduled task prompt is required" });
2721
3540
  }
2722
3541
  await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
2723
3542
  if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
2724
- throw new HTTPException9(503, { message: "object storage is not configured" });
3543
+ throw new HTTPException10(503, { message: "object storage is not configured" });
2725
3544
  }
2726
3545
  await validateFileResources(input.db, input.workspaceId, resources);
2727
3546
  return {
@@ -2736,19 +3555,19 @@ function validateScheduledTaskSchedule(schedule) {
2736
3555
  return;
2737
3556
  }
2738
3557
  if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
2739
- throw new HTTPException9(422, { message: "interval schedule endAt must be after startAt" });
3558
+ throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
2740
3559
  }
2741
3560
  }
2742
3561
  function trimmedScheduledTaskName(name) {
2743
3562
  const trimmed = name.trim();
2744
3563
  if (!trimmed) {
2745
- throw new HTTPException9(422, { message: "scheduled task name is required" });
3564
+ throw new HTTPException10(422, { message: "scheduled task name is required" });
2746
3565
  }
2747
3566
  return trimmed;
2748
3567
  }
2749
3568
 
2750
3569
  // src/domain/workspace-members.ts
2751
- import { HTTPException as HTTPException10 } from "hono/http-exception";
3570
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
2752
3571
  var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
2753
3572
  function memberCanAdminister(member) {
2754
3573
  return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
@@ -2758,55 +3577,71 @@ function isUserMember(member) {
2758
3577
  }
2759
3578
  function resolveMemberSubjectId(userId) {
2760
3579
  if (!userId) {
2761
- throw new HTTPException10(404, { message: "user is not registered" });
3580
+ throw new HTTPException11(404, { message: "user is not registered" });
2762
3581
  }
2763
3582
  return `user:${userId}`;
2764
3583
  }
2765
3584
  function assertWorkspaceMemberRemovable(input) {
2766
3585
  const { members, subjectId, callerSubjectId } = input;
2767
3586
  if (subjectId === callerSubjectId) {
2768
- throw new HTTPException10(409, { message: "you cannot remove your own membership" });
3587
+ throw new HTTPException11(409, { message: "you cannot remove your own membership" });
2769
3588
  }
2770
3589
  const target = members.find((member) => member.subjectId === subjectId);
2771
3590
  if (!target) {
2772
- throw new HTTPException10(404, { message: "member not found" });
3591
+ throw new HTTPException11(404, { message: "member not found" });
2773
3592
  }
2774
3593
  if (memberCanAdminister(target)) {
2775
- const remainingAdmins = members.filter((member) => member.subjectId !== subjectId && memberCanAdminister(member));
3594
+ const remainingAdmins = members.filter(
3595
+ (member) => member.subjectId !== subjectId && memberCanAdminister(member)
3596
+ );
2776
3597
  if (remainingAdmins.length === 0) {
2777
- throw new HTTPException10(409, { message: "cannot remove the last member who can manage this workspace" });
3598
+ throw new HTTPException11(409, {
3599
+ message: "cannot remove the last member who can manage this workspace"
3600
+ });
2778
3601
  }
2779
3602
  }
2780
3603
  }
2781
3604
  function assertWorkspaceDeletable(input) {
2782
3605
  if (input.workspaceCountForAccount <= 1) {
2783
- throw new HTTPException10(409, { message: "cannot delete the account's only workspace" });
3606
+ throw new HTTPException11(409, { message: "cannot delete the account's only workspace" });
2784
3607
  }
2785
3608
  if (input.activeSessionCount > 0) {
2786
- throw new HTTPException10(409, {
3609
+ throw new HTTPException11(409, {
2787
3610
  message: "stop the workspace's running sessions before deleting it"
2788
3611
  });
2789
3612
  }
2790
3613
  }
2791
3614
  export {
2792
3615
  MARKETING_SOCIAL_PACK_ID,
3616
+ MAX_CHECKS_PER_RIG,
3617
+ MAX_CREDENTIAL_HOOKS_PER_RIG,
3618
+ MAX_DEFAULT_VARIABLE_SETS_PER_RIG,
2793
3619
  MAX_ENVIRONMENTS_PER_WORKSPACE,
3620
+ MAX_RIGS_PER_WORKSPACE,
2794
3621
  MAX_VARIABLES_PER_ENVIRONMENT,
2795
3622
  acceptSessionUserMessage,
3623
+ activateRigVersionForApi,
3624
+ appendRigSetupCommand,
2796
3625
  applyCapabilityEnablement,
2797
3626
  assertAllowedEnvironmentVariableName,
3627
+ assertAllowedVariableSetVariableName,
2798
3628
  assertConfiguredModel,
2799
3629
  assertPackSandboxImageCompatible,
2800
3630
  assertWorkspaceDeletable,
2801
3631
  assertWorkspaceMemberRemovable,
3632
+ assertWorkspaceModelPolicyAllows,
2802
3633
  buildCapabilityCatalog,
2803
3634
  buildFleetContextForSession,
2804
3635
  buildMarketingDailyAnalysisAgentConfig,
2805
3636
  checkLimit,
3637
+ classifyRigVerificationOutcome,
2806
3638
  createAndStartSession,
2807
3639
  createCatalogItem,
3640
+ createRigForApi,
3641
+ createRigVersionForApi,
2808
3642
  createSessionForRequest,
2809
3643
  createValidatedScheduledTask,
3644
+ deleteRigForApi,
2810
3645
  disableCapability,
2811
3646
  discoverMcpRegistryCapabilities,
2812
3647
  enableCapability,
@@ -2817,6 +3652,8 @@ export {
2817
3652
  isUserMember,
2818
3653
  listCapabilityPacks,
2819
3654
  listFleet,
3655
+ listRigChangesForApi,
3656
+ listRigVersionsForApi,
2820
3657
  listWorkspaceCapabilityPacks,
2821
3658
  manualScheduledTaskTriggerUsageKey,
2822
3659
  manualScheduledTaskTriggerWorkflowId,
@@ -2826,23 +3663,32 @@ export {
2826
3663
  normalizeResources,
2827
3664
  officialMcpRegistryUrl,
2828
3665
  postUserMessageTurn,
3666
+ promoteSetupAppendChange,
3667
+ promoteVerifiedDefinitionEditChangeForApi,
3668
+ proposeRigChangeForApi,
2829
3669
  provisionSandbox,
3670
+ readSessionLineage,
2830
3671
  reasoningEffortForSession,
2831
- recordEnvironmentAuditEvent,
3672
+ recordRigAuditEvent,
3673
+ recordVariableSetAuditEvent,
2832
3674
  recordWorkspaceUsage,
2833
3675
  relayConfigFromSettings,
2834
3676
  relayDialBaseFromSettings,
2835
3677
  requireAccessContext,
2836
3678
  requireAccessGrant,
2837
3679
  requireEnvironmentEncryption,
2838
- requireEnvironmentForApi,
2839
3680
  requireLimit,
2840
3681
  requirePermission,
2841
3682
  requireQueuedTurnForApi,
3683
+ requireRigChangeForApi,
3684
+ requireRigForApi,
2842
3685
  requireScheduledTaskForApi,
3686
+ requireVariableSetEncryption,
3687
+ requireVariableSetForApi,
2843
3688
  resolveCapabilityPack,
2844
3689
  resolveMemberSubjectId,
2845
3690
  restoreScheduledTask,
3691
+ rigActorForGrant,
2846
3692
  routingEnabled,
2847
3693
  runOnSandbox,
2848
3694
  scheduledTaskTemporalScheduleId,
@@ -2855,13 +3701,14 @@ export {
2855
3701
  swapActiveSandbox,
2856
3702
  syncCreatedScheduledTask,
2857
3703
  syncUpdatedScheduledTask,
3704
+ updateRigForApi,
2858
3705
  updateSessionTitle,
2859
- validateEnvironmentAttachment,
2860
3706
  validateFileResources,
2861
3707
  validateGitHubRepositorySelection,
2862
3708
  validateGitHubRepositorySelectionShape,
2863
3709
  validateMcpCapabilityConnection,
2864
3710
  validateToolRefs,
3711
+ validateVariableSetAttachment,
2865
3712
  validatedScheduledTaskUpdate,
2866
3713
  withDefaultEnabledCapabilityMcpTools,
2867
3714
  workflowIdForSession,