@opengeni/core 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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({
@@ -1245,7 +1469,10 @@ async function validateMcpCapabilityConnection(item, probe = probeStreamableHttp
1245
1469
  async function probeStreamableHttpMcpServer(input) {
1246
1470
  const controller = new AbortController();
1247
1471
  const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
1248
- const client = new Client({ 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,8 +1480,14 @@ 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);
@@ -1264,18 +1497,32 @@ async function probeStreamableHttpMcpServer(input) {
1264
1497
  function mcpProbeErrorMessage(error, endpointUrl) {
1265
1498
  const message = error instanceof Error ? error.message : String(error);
1266
1499
  const normalized = message.replace(/\s+/g, " ").trim();
1267
- if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(normalized)) {
1500
+ if (/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
1501
+ normalized
1502
+ )) {
1268
1503
  return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
1269
1504
  }
1270
1505
  return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
1271
1506
  }
1272
1507
  async function disableCapability(input) {
1273
- const item = await requireCatalogItem(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
+ );
1274
1514
  if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
1275
- 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
+ });
1276
1518
  }
1277
1519
  if (item.kind === "pack") {
1278
- 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);
1279
1526
  if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
1280
1527
  await enableCapabilityInstallation(input.db, {
1281
1528
  accountId: input.accountId,
@@ -1306,16 +1553,18 @@ function settingsWithMcpCapabilityServers(settings, enabled) {
1306
1553
  if (headers === "unavailable" && !server.connectionRef) {
1307
1554
  return [];
1308
1555
  }
1309
- return [{
1310
- id: server.id,
1311
- name: server.name,
1312
- url: server.url,
1313
- ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
1314
- ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
1315
- cacheToolsList: server.cacheToolsList ?? false,
1316
- ...headers && headers !== "unavailable" ? { headers } : {},
1317
- ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
1318
- }];
1556
+ return [
1557
+ {
1558
+ id: server.id,
1559
+ name: server.name,
1560
+ url: server.url,
1561
+ ...server.allowedTools ? { allowedTools: server.allowedTools } : {},
1562
+ ...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
1563
+ cacheToolsList: server.cacheToolsList ?? false,
1564
+ ...headers && headers !== "unavailable" ? { headers } : {},
1565
+ ...server.connectionRef ? { connectionRef: server.connectionRef } : {}
1566
+ }
1567
+ ];
1319
1568
  });
1320
1569
  return dynamicServers.length ? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] } : settings;
1321
1570
  }
@@ -1369,7 +1618,10 @@ async function discoverMcpRegistryCapabilities(input) {
1369
1618
  async function fetchMcpRegistryPage(url, options = {}) {
1370
1619
  const fetchImpl = options.fetchImpl ?? fetch;
1371
1620
  const controller = new AbortController();
1372
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? mcpRegistryFetchTimeoutMs);
1621
+ const timeout = setTimeout(
1622
+ () => controller.abort(),
1623
+ options.timeoutMs ?? mcpRegistryFetchTimeoutMs
1624
+ );
1373
1625
  try {
1374
1626
  const response = await fetchImpl(url, { signal: controller.signal });
1375
1627
  if (!response.ok) {
@@ -1426,28 +1678,30 @@ function packCatalogItem(pack, source) {
1426
1678
  });
1427
1679
  }
1428
1680
  function configuredMcpCatalogItems(settings) {
1429
- return settings.mcpServers.map((server) => CapabilityCatalogItem.parse({
1430
- id: `mcp:${server.id}`,
1431
- kind: "mcp",
1432
- source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
1433
- name: server.name ?? server.id,
1434
- description: firstPartyMcpDescription(server.id),
1435
- category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
1436
- tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
1437
- endpointUrl: server.url,
1438
- tools: [{ kind: "mcp", id: server.id }],
1439
- runtime: {
1440
- available: true,
1441
- mcpServerId: server.id,
1442
- transport: "streamable-http",
1443
- notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
1444
- },
1445
- metadata: {
1446
- mcpServerId: server.id,
1447
- allowedTools: server.allowedTools ?? [],
1448
- cacheToolsList: server.cacheToolsList
1449
- }
1450
- }));
1681
+ return settings.mcpServers.map(
1682
+ (server) => CapabilityCatalogItem.parse({
1683
+ id: `mcp:${server.id}`,
1684
+ kind: "mcp",
1685
+ source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
1686
+ name: server.name ?? server.id,
1687
+ description: firstPartyMcpDescription(server.id),
1688
+ category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
1689
+ tags: ["mcp", ...server.allowedTools?.length ? ["limited-tools"] : []],
1690
+ endpointUrl: server.url,
1691
+ tools: [{ kind: "mcp", id: server.id }],
1692
+ runtime: {
1693
+ available: true,
1694
+ mcpServerId: server.id,
1695
+ transport: "streamable-http",
1696
+ notes: firstPartyMcpServerIds.has(server.id) ? "Available from OpenGeni runtime configuration." : "Configured through OPENGENI_MCP_SERVERS."
1697
+ },
1698
+ metadata: {
1699
+ mcpServerId: server.id,
1700
+ allowedTools: server.allowedTools ?? [],
1701
+ cacheToolsList: server.cacheToolsList
1702
+ }
1703
+ })
1704
+ );
1451
1705
  }
1452
1706
  function platformApiCatalogItems() {
1453
1707
  return [
@@ -1483,46 +1737,56 @@ function platformApiCatalogItems() {
1483
1737
  tags: ["api", "schedules", "agents"],
1484
1738
  endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks"
1485
1739
  }
1486
- ].map((item) => CapabilityCatalogItem.parse({
1487
- id: item.id,
1488
- name: item.name,
1489
- description: item.description,
1490
- category: item.category,
1491
- tags: item.tags,
1492
- kind: "api",
1493
- source: "built_in",
1494
- runtime: {
1495
- available: true,
1496
- notes: "Available through the OpenGeni API."
1497
- },
1498
- metadata: {
1499
- endpointPath: item.endpointPath
1500
- }
1501
- }));
1740
+ ].map(
1741
+ (item) => CapabilityCatalogItem.parse({
1742
+ id: item.id,
1743
+ name: item.name,
1744
+ description: item.description,
1745
+ category: item.category,
1746
+ tags: item.tags,
1747
+ kind: "api",
1748
+ source: "built_in",
1749
+ runtime: {
1750
+ available: true,
1751
+ notes: "Available through the OpenGeni API."
1752
+ },
1753
+ metadata: {
1754
+ endpointPath: item.endpointPath
1755
+ }
1756
+ })
1757
+ );
1502
1758
  }
1503
1759
  async function discoverBundledSkills() {
1504
- const skillsDir = new URL("../../../../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
+ );
1505
1764
  try {
1506
1765
  const entries = await readdir(skillsDir, { withFileTypes: true });
1507
- const skills = await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1508
- const skill = await readSkillMetadata(new URL(`${entry.name}/SKILL.md`, skillsDir), entry.name);
1509
- return CapabilityCatalogItem.parse({
1510
- id: `skill:${entry.name}`,
1511
- kind: "skill",
1512
- source: "built_in",
1513
- name: skill.name,
1514
- description: skill.description,
1515
- category: skill.category,
1516
- tags: ["skill", skill.category],
1517
- runtime: {
1518
- available: true,
1519
- notes: "Bundled into the sandbox skill library."
1520
- },
1521
- metadata: {
1522
- path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
1523
- }
1524
- });
1525
- }));
1766
+ const skills = await Promise.all(
1767
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1768
+ const skill = await readSkillMetadata(
1769
+ new URL(`${entry.name}/SKILL.md`, skillsDir),
1770
+ entry.name
1771
+ );
1772
+ return CapabilityCatalogItem.parse({
1773
+ id: `skill:${entry.name}`,
1774
+ kind: "skill",
1775
+ source: "built_in",
1776
+ name: skill.name,
1777
+ description: skill.description,
1778
+ category: skill.category,
1779
+ tags: ["skill", skill.category],
1780
+ runtime: {
1781
+ available: true,
1782
+ notes: "Bundled into the sandbox skill library."
1783
+ },
1784
+ metadata: {
1785
+ path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`
1786
+ }
1787
+ });
1788
+ })
1789
+ );
1526
1790
  return skills;
1527
1791
  } catch {
1528
1792
  return [];
@@ -1599,7 +1863,11 @@ function firstPartyMcpDescription(id) {
1599
1863
  return null;
1600
1864
  }
1601
1865
  function generatedCapabilityId(payload) {
1602
- const source = [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(":");
1603
1871
  return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
1604
1872
  }
1605
1873
  function publicRegistryCapabilityId(name, version, endpointUrl) {
@@ -1637,7 +1905,9 @@ function mcpRegistryEntryToCatalogItem(entry) {
1637
1905
  if (official?.isLatest === false) {
1638
1906
  return null;
1639
1907
  }
1640
- const remote = server.remotes?.find((candidate) => candidate.type === "streamable-http" && candidate.url);
1908
+ const remote = server.remotes?.find(
1909
+ (candidate) => candidate.type === "streamable-http" && candidate.url
1910
+ );
1641
1911
  const endpointUrl = validUrl(remote?.url);
1642
1912
  if (!remote || !endpointUrl) {
1643
1913
  return null;
@@ -1654,7 +1924,12 @@ function mcpRegistryEntryToCatalogItem(entry) {
1654
1924
  name: server.title || server.name,
1655
1925
  description: server.description ?? null,
1656
1926
  category: "public-mcp",
1657
- tags: ["mcp", "public", "registry", ...requiredHeaders.length ? ["requires-credentials"] : []],
1927
+ tags: [
1928
+ "mcp",
1929
+ "public",
1930
+ "registry",
1931
+ ...requiredHeaders.length ? ["requires-credentials"] : []
1932
+ ],
1658
1933
  homepageUrl,
1659
1934
  endpointUrl,
1660
1935
  installUrl: homepageUrl,
@@ -1732,6 +2007,367 @@ function storedConnectionRef(config) {
1732
2007
  return !!ref && typeof ref === "object" && !Array.isArray(ref) && typeof ref.providerDomain === "string";
1733
2008
  }
1734
2009
 
2010
+ // src/rigs/index.ts
2011
+ import {
2012
+ activateRigVersion,
2013
+ countRigs,
2014
+ createRig,
2015
+ createRigChange,
2016
+ createRigVersion,
2017
+ createRigVersionForChangePromotion,
2018
+ deleteRigIfNoActiveSessions,
2019
+ getRig,
2020
+ getRigByName,
2021
+ getRigChange,
2022
+ getRigVersion,
2023
+ getVariableSet as getVariableSet3,
2024
+ listRigChanges,
2025
+ listRigVersions,
2026
+ recordAuditEvent as recordAuditEvent2,
2027
+ RigActiveVersionChangedError,
2028
+ RigChangeTransitionError,
2029
+ updateRig
2030
+ } from "@opengeni/db";
2031
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
2032
+ var MAX_RIGS_PER_WORKSPACE = 50;
2033
+ var MAX_CHECKS_PER_RIG = 100;
2034
+ var MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
2035
+ var MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
2036
+ async function recordRigAuditEvent(db, input) {
2037
+ await recordAuditEvent2(db, {
2038
+ accountId: input.grant.accountId,
2039
+ workspaceId: input.grant.workspaceId,
2040
+ subjectId: input.grant.subjectId,
2041
+ action: input.action,
2042
+ targetType: "rig",
2043
+ targetId: input.rigId,
2044
+ metadata: { rigId: input.rigId, ...input.metadata ?? {} }
2045
+ });
2046
+ }
2047
+ function rigActorForGrant(grant) {
2048
+ return `user:${grant.subjectId}`;
2049
+ }
2050
+ async function requireRigForApi(db, workspaceId, rigId) {
2051
+ const rig = await getRig(db, workspaceId, rigId);
2052
+ if (!rig) {
2053
+ throw new HTTPException7(404, { message: "rig not found" });
2054
+ }
2055
+ return rig;
2056
+ }
2057
+ async function requireRigChangeForApi(db, workspaceId, rigId, changeId) {
2058
+ const change = await getRigChange(db, workspaceId, changeId);
2059
+ if (!change || change.rigId !== rigId) {
2060
+ throw new HTTPException7(404, { message: "rig change not found" });
2061
+ }
2062
+ return change;
2063
+ }
2064
+ function trimmedRigName(name) {
2065
+ const trimmed = name.trim();
2066
+ if (!trimmed) {
2067
+ throw new HTTPException7(422, { message: "rig name is required" });
2068
+ }
2069
+ return trimmed;
2070
+ }
2071
+ function assertUniqueCheckNames(checks) {
2072
+ if (!checks) {
2073
+ return;
2074
+ }
2075
+ const seen = /* @__PURE__ */ new Set();
2076
+ for (const check of checks) {
2077
+ if (seen.has(check.name)) {
2078
+ throw new HTTPException7(422, { message: `duplicate rig check name: ${check.name}` });
2079
+ }
2080
+ seen.add(check.name);
2081
+ }
2082
+ }
2083
+ async function assertVariableSetsExist(db, workspaceId, ids) {
2084
+ if (!ids || ids.length === 0) {
2085
+ return;
2086
+ }
2087
+ const unique = [...new Set(ids)];
2088
+ for (const id of unique) {
2089
+ const variableSet = await getVariableSet3(db, workspaceId, id);
2090
+ if (!variableSet) {
2091
+ throw new HTTPException7(422, { message: `unknown defaultVariableSetId: ${id}` });
2092
+ }
2093
+ }
2094
+ }
2095
+ async function createRigForApi(deps, grant, payload) {
2096
+ const workspaceId = grant.workspaceId;
2097
+ const name = trimmedRigName(payload.name);
2098
+ assertUniqueCheckNames(payload.checks);
2099
+ await assertVariableSetsExist(deps.db, workspaceId, payload.defaultVariableSetIds);
2100
+ if (await countRigs(deps.db, workspaceId) >= MAX_RIGS_PER_WORKSPACE) {
2101
+ throw new HTTPException7(422, {
2102
+ message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`
2103
+ });
2104
+ }
2105
+ if (await getRigByName(deps.db, workspaceId, name)) {
2106
+ throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
2107
+ }
2108
+ const createdBy = rigActorForGrant(grant);
2109
+ const rig = await createRig(deps.db, {
2110
+ accountId: grant.accountId,
2111
+ workspaceId,
2112
+ name,
2113
+ description: payload.description ?? null,
2114
+ createdBy,
2115
+ initialVersion: {
2116
+ image: payload.image ?? null,
2117
+ setupScript: payload.setupScript ?? null,
2118
+ checks: payload.checks,
2119
+ credentialHooks: payload.credentialHooks,
2120
+ defaultVariableSetIds: payload.defaultVariableSetIds,
2121
+ changelog: "Initial version",
2122
+ createdBy
2123
+ }
2124
+ });
2125
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.created", rigId: rig.id });
2126
+ return rig;
2127
+ }
2128
+ async function updateRigForApi(deps, grant, rig, payload) {
2129
+ const workspaceId = grant.workspaceId;
2130
+ const name = payload.name !== void 0 ? trimmedRigName(payload.name) : void 0;
2131
+ if (name !== void 0 && name !== rig.name) {
2132
+ const existing = await getRigByName(deps.db, workspaceId, name);
2133
+ if (existing && existing.id !== rig.id) {
2134
+ throw new HTTPException7(409, { message: `rig name is already in use: ${name}` });
2135
+ }
2136
+ }
2137
+ const updated = await updateRig(deps.db, workspaceId, rig.id, {
2138
+ ...name !== void 0 ? { name } : {},
2139
+ ...payload.description !== void 0 ? { description: payload.description } : {}
2140
+ });
2141
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.updated", rigId: rig.id });
2142
+ return updated;
2143
+ }
2144
+ async function deleteRigForApi(deps, grant, rig) {
2145
+ const workspaceId = grant.workspaceId;
2146
+ const deleted = await deleteRigIfNoActiveSessions(deps.db, workspaceId, rig.id);
2147
+ if (deleted.activeSessionCount > 0) {
2148
+ throw new HTTPException7(409, {
2149
+ message: `rig is referenced by ${deleted.activeSessionCount} active session(s); it cannot be deleted`
2150
+ });
2151
+ }
2152
+ if (!deleted.deleted) {
2153
+ throw new HTTPException7(404, { message: "rig not found" });
2154
+ }
2155
+ await recordRigAuditEvent(deps.db, { grant, action: "rig.deleted", rigId: rig.id });
2156
+ }
2157
+ async function proposeRigChangeForApi(deps, grant, rig, request, options = {}) {
2158
+ const workspaceId = grant.workspaceId;
2159
+ if (!rig.activeVersion) {
2160
+ throw new HTTPException7(422, { message: "rig has no active version to base a change on" });
2161
+ }
2162
+ if (request.kind === "definition_edit") {
2163
+ assertUniqueCheckNames(request.payload.checks);
2164
+ await assertVariableSetsExist(
2165
+ deps.db,
2166
+ workspaceId,
2167
+ request.payload.defaultVariableSetIds ?? void 0
2168
+ );
2169
+ }
2170
+ const change = await createRigChange(deps.db, {
2171
+ accountId: grant.accountId,
2172
+ workspaceId,
2173
+ rigId: rig.id,
2174
+ baseVersionId: rig.activeVersion.id,
2175
+ kind: request.kind,
2176
+ payload: request.payload,
2177
+ proposedBy: options.proposedBy ?? rigActorForGrant(grant)
2178
+ });
2179
+ await recordRigAuditEvent(deps.db, {
2180
+ grant,
2181
+ action: "rig.change.proposed",
2182
+ rigId: rig.id,
2183
+ metadata: { changeId: change.id, kind: change.kind }
2184
+ });
2185
+ return change;
2186
+ }
2187
+ function classifyRigVerificationOutcome(input) {
2188
+ if (input.infraError) {
2189
+ return { status: "failed", action: "retryable_failure" };
2190
+ }
2191
+ if (!input.passed) {
2192
+ return { status: "rejected", action: "reject" };
2193
+ }
2194
+ if (input.kind === "setup_append") {
2195
+ return { status: "merged", action: "auto_promote" };
2196
+ }
2197
+ return { status: "proposed", action: "await_manage_promote" };
2198
+ }
2199
+ function appendRigSetupCommand(baseSetupScript, command) {
2200
+ const base = (baseSetupScript ?? "").trimEnd();
2201
+ return base ? `${base}
2202
+ ${command}` : command;
2203
+ }
2204
+ async function promoteChangeWithActiveCas(deps, workspaceId, rigId, changeId, input) {
2205
+ try {
2206
+ return await createRigVersionForChangePromotion(deps.db, workspaceId, rigId, changeId, input);
2207
+ } catch (error) {
2208
+ if (error instanceof RigActiveVersionChangedError) {
2209
+ throw new HTTPException7(409, {
2210
+ message: `rig moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`
2211
+ });
2212
+ }
2213
+ if (error instanceof RigChangeTransitionError) {
2214
+ throw new HTTPException7(409, { message: error.message });
2215
+ }
2216
+ throw error;
2217
+ }
2218
+ }
2219
+ async function promoteSetupAppendChange(deps, grant, rig, change) {
2220
+ if (change.kind !== "setup_append") {
2221
+ throw new HTTPException7(422, {
2222
+ message: "only setup_append changes auto-promote through this path"
2223
+ });
2224
+ }
2225
+ if (change.status !== "proposed" && change.status !== "verifying") {
2226
+ throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
2227
+ }
2228
+ if (!change.baseVersionId) {
2229
+ throw new HTTPException7(422, { message: "rig change has no base version" });
2230
+ }
2231
+ const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
2232
+ if (!base) {
2233
+ throw new HTTPException7(404, { message: "base rig version not found" });
2234
+ }
2235
+ const payload = change.payload;
2236
+ if (typeof payload.command !== "string" || !payload.command.trim()) {
2237
+ throw new HTTPException7(422, { message: "setup_append change is missing command" });
2238
+ }
2239
+ const { version, change: updated } = await promoteChangeWithActiveCas(
2240
+ deps,
2241
+ grant.workspaceId,
2242
+ rig.id,
2243
+ change.id,
2244
+ {
2245
+ expectedActiveVersionId: change.baseVersionId,
2246
+ image: base.image,
2247
+ setupScript: appendRigSetupCommand(base.setupScript, payload.command),
2248
+ checks: base.checks,
2249
+ credentialHooks: base.credentialHooks,
2250
+ defaultVariableSetIds: base.defaultVariableSetIds,
2251
+ changelog: typeof payload.note === "string" && payload.note.trim() ? payload.note : "Verified setup append",
2252
+ createdBy: change.proposedBy ?? rigActorForGrant(grant)
2253
+ }
2254
+ );
2255
+ await recordRigAuditEvent(deps.db, {
2256
+ grant,
2257
+ action: "rig.change.merged",
2258
+ rigId: rig.id,
2259
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2260
+ });
2261
+ await recordRigAuditEvent(deps.db, {
2262
+ grant,
2263
+ action: "rig.version.promoted",
2264
+ rigId: rig.id,
2265
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2266
+ });
2267
+ return { change: updated, version };
2268
+ }
2269
+ async function promoteVerifiedDefinitionEditChangeForApi(deps, grant, rig, change) {
2270
+ if (change.kind !== "definition_edit") {
2271
+ throw new HTTPException7(422, { message: "only definition_edit changes use explicit promote" });
2272
+ }
2273
+ if (change.status !== "proposed") {
2274
+ throw new HTTPException7(409, { message: `rig change is ${change.status}; cannot promote` });
2275
+ }
2276
+ if (change.verification?.passed !== true) {
2277
+ throw new HTTPException7(422, {
2278
+ message: "definition_edit change must pass verification before promote"
2279
+ });
2280
+ }
2281
+ if (!change.baseVersionId) {
2282
+ throw new HTTPException7(422, { message: "rig change has no base version" });
2283
+ }
2284
+ const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
2285
+ if (!base) {
2286
+ throw new HTTPException7(404, { message: "base rig version not found" });
2287
+ }
2288
+ const payload = change.payload;
2289
+ const { version, change: updated } = await promoteChangeWithActiveCas(
2290
+ deps,
2291
+ grant.workspaceId,
2292
+ rig.id,
2293
+ change.id,
2294
+ {
2295
+ expectedActiveVersionId: change.baseVersionId,
2296
+ image: payload.image === void 0 ? base.image : payload.image,
2297
+ setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
2298
+ checks: Array.isArray(payload.checks) ? payload.checks : base.checks,
2299
+ credentialHooks: Array.isArray(payload.credentialHooks) ? payload.credentialHooks : base.credentialHooks,
2300
+ defaultVariableSetIds: Array.isArray(payload.defaultVariableSetIds) ? payload.defaultVariableSetIds : base.defaultVariableSetIds,
2301
+ changelog: typeof payload.changelog === "string" && payload.changelog.trim() ? payload.changelog : "Verified definition edit",
2302
+ createdBy: rigActorForGrant(grant)
2303
+ }
2304
+ );
2305
+ await recordRigAuditEvent(deps.db, {
2306
+ grant,
2307
+ action: "rig.change.merged",
2308
+ rigId: rig.id,
2309
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2310
+ });
2311
+ await recordRigAuditEvent(deps.db, {
2312
+ grant,
2313
+ action: "rig.version.promoted",
2314
+ rigId: rig.id,
2315
+ metadata: { changeId: change.id, versionId: version.id, version: version.version }
2316
+ });
2317
+ return { change: updated, version };
2318
+ }
2319
+ async function createRigVersionForApi(deps, grant, rig, payload) {
2320
+ if (!rig.activeVersion) {
2321
+ throw new HTTPException7(422, { message: "rig has no active version" });
2322
+ }
2323
+ assertUniqueCheckNames(payload.checks);
2324
+ await assertVariableSetsExist(
2325
+ deps.db,
2326
+ grant.workspaceId,
2327
+ payload.defaultVariableSetIds ?? void 0
2328
+ );
2329
+ const base = rig.activeVersion;
2330
+ const version = await createRigVersion(
2331
+ deps.db,
2332
+ grant.workspaceId,
2333
+ rig.id,
2334
+ {
2335
+ image: payload.image === void 0 ? base.image : payload.image,
2336
+ setupScript: payload.setupScript === void 0 ? base.setupScript : payload.setupScript,
2337
+ checks: payload.checks ?? base.checks,
2338
+ credentialHooks: payload.credentialHooks ?? base.credentialHooks,
2339
+ defaultVariableSetIds: payload.defaultVariableSetIds ?? base.defaultVariableSetIds,
2340
+ changelog: payload.changelog ?? "Manager-created version",
2341
+ createdBy: rigActorForGrant(grant)
2342
+ },
2343
+ { activate: true }
2344
+ );
2345
+ await recordRigAuditEvent(deps.db, {
2346
+ grant,
2347
+ action: "rig.version.promoted",
2348
+ rigId: rig.id,
2349
+ metadata: { versionId: version.id, version: version.version, direct: true }
2350
+ });
2351
+ return version;
2352
+ }
2353
+ async function activateRigVersionForApi(deps, grant, rig, versionId) {
2354
+ const workspaceId = grant.workspaceId;
2355
+ const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
2356
+ await recordRigAuditEvent(deps.db, {
2357
+ grant,
2358
+ action: "rig.version.activated",
2359
+ rigId: rig.id,
2360
+ metadata: { versionId: version.id, version: version.version }
2361
+ });
2362
+ return version;
2363
+ }
2364
+ async function listRigVersionsForApi(deps, workspaceId, rigId) {
2365
+ return await listRigVersions(deps.db, workspaceId, rigId);
2366
+ }
2367
+ async function listRigChangesForApi(deps, workspaceId, rigId, limit) {
2368
+ return await listRigChanges(deps.db, workspaceId, rigId, limit);
2369
+ }
2370
+
1735
2371
  // src/domain/resources.ts
1736
2372
  import {
1737
2373
  mergeResourceRefs as mergeContractResourceRefs,
@@ -1740,26 +2376,27 @@ import {
1740
2376
  ResourceRefConflictError,
1741
2377
  stableJson
1742
2378
  } from "@opengeni/contracts";
1743
- import {
1744
- listGitHubInstallationIdsForWorkspace,
1745
- requireFile
1746
- } from "@opengeni/db";
1747
- import { HTTPException as HTTPException7 } from "hono/http-exception";
2379
+ import { listGitHubInstallationIdsForWorkspace, requireFile } from "@opengeni/db";
2380
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
1748
2381
  function validateToolRefs(tools, settings) {
1749
2382
  const mcpServerIds = new Set(settings.mcpServers.map((server) => server.id));
1750
2383
  const out = [];
1751
2384
  for (const tool of tools) {
1752
2385
  if (tool.kind !== "mcp") {
1753
- throw new HTTPException7(422, { message: `unsupported tool kind: ${tool.kind}` });
2386
+ throw new HTTPException8(422, {
2387
+ message: `unsupported tool kind: ${tool.kind}`
2388
+ });
1754
2389
  }
1755
2390
  const optional = tool.optional === true;
1756
2391
  if (!mcpServerIds.has(tool.id)) {
1757
2392
  if (optional) {
1758
2393
  continue;
1759
2394
  }
1760
- throw new HTTPException7(422, { message: `unknown MCP server id: ${tool.id}` });
2395
+ throw new HTTPException8(422, { message: `unknown MCP server id: ${tool.id}` });
1761
2396
  }
1762
- 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
+ );
1763
2400
  }
1764
2401
  return mergeToolRefs([], out);
1765
2402
  }
@@ -1787,12 +2424,12 @@ function normalizeResources(resources) {
1787
2424
  } else {
1788
2425
  const url = parseResourceUrl(resource.uri);
1789
2426
  if (url.protocol !== "https:" || !url.hostname) {
1790
- throw new HTTPException7(422, { message: "repository resources must use HTTPS Git URLs" });
2427
+ throw new HTTPException8(422, { message: "repository resources must use HTTPS Git URLs" });
1791
2428
  }
1792
2429
  const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
1793
2430
  const parts = path.split("/").filter(Boolean);
1794
2431
  if (parts.length < 2) {
1795
- throw new HTTPException7(422, { message: "repository URL must include owner and repo" });
2432
+ throw new HTTPException8(422, { message: "repository URL must include owner and repo" });
1796
2433
  }
1797
2434
  const repo = parts.join("/");
1798
2435
  const mountPath = normalizeMountPath(resource.mountPath ?? `repos/${repo}`);
@@ -1802,6 +2439,11 @@ function normalizeResources(resources) {
1802
2439
  ref: resource.ref.trim(),
1803
2440
  mountPath,
1804
2441
  ...resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {},
2442
+ ...resource.provider ? { provider: resource.provider } : {},
2443
+ ...resource.repositoryId !== void 0 ? { repositoryId: resource.repositoryId } : {},
2444
+ ...resource.installationId !== void 0 ? { installationId: resource.installationId } : {},
2445
+ ...resource.projectId !== void 0 ? { projectId: resource.projectId } : {},
2446
+ ...resource.connectionId ? { connectionId: resource.connectionId } : {},
1805
2447
  ...resource.githubInstallationId ? { githubInstallationId: resource.githubInstallationId } : {},
1806
2448
  ...resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}
1807
2449
  };
@@ -1809,7 +2451,9 @@ function normalizeResources(resources) {
1809
2451
  const key = stableJson(normalized);
1810
2452
  const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : void 0;
1811
2453
  if (mounted && mounted !== key) {
1812
- throw new HTTPException7(422, { message: `duplicate resource mount path: ${normalized.mountPath}` });
2454
+ throw new HTTPException8(422, {
2455
+ message: `duplicate resource mount path: ${normalized.mountPath}`
2456
+ });
1813
2457
  }
1814
2458
  if (normalized.mountPath) {
1815
2459
  mountPaths.set(normalized.mountPath, key);
@@ -1817,7 +2461,9 @@ function normalizeResources(resources) {
1817
2461
  const identity = resourceIdentityKey(normalized);
1818
2462
  const seenIdentity = identities.get(identity);
1819
2463
  if (seenIdentity && seenIdentity !== key) {
1820
- throw new HTTPException7(422, { message: `duplicate resource with different settings: ${identity}` });
2464
+ throw new HTTPException8(422, {
2465
+ message: `duplicate resource with different settings: ${identity}`
2466
+ });
1821
2467
  }
1822
2468
  identities.set(identity, key);
1823
2469
  if (!seenResources.has(key)) {
@@ -1832,7 +2478,7 @@ function mergeResourceRefs(existing, additions) {
1832
2478
  return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
1833
2479
  } catch (error) {
1834
2480
  if (error instanceof ResourceRefConflictError) {
1835
- throw new HTTPException7(422, { message: error.message });
2481
+ throw new HTTPException8(422, { message: error.message });
1836
2482
  }
1837
2483
  throw error;
1838
2484
  }
@@ -1842,8 +2488,8 @@ function validateGitHubRepositorySelectionShape(resources) {
1842
2488
  if (resource.kind !== "repository") {
1843
2489
  return [];
1844
2490
  }
1845
- const installationRaw = resource.githubInstallationId;
1846
- const repositoryRaw = resource.githubRepositoryId;
2491
+ const installationRaw = resource.githubInstallationId ?? (resource.provider === "github" ? resource.installationId : void 0);
2492
+ const repositoryRaw = resource.githubRepositoryId ?? (resource.provider === "github" ? resource.repositoryId : void 0);
1847
2493
  if (installationRaw === null && repositoryRaw === null) {
1848
2494
  return [];
1849
2495
  }
@@ -1853,7 +2499,7 @@ function validateGitHubRepositorySelectionShape(resources) {
1853
2499
  const installationId2 = positiveInteger(installationRaw);
1854
2500
  const repositoryId = positiveInteger(repositoryRaw);
1855
2501
  if (!installationId2 || !repositoryId) {
1856
- throw new HTTPException7(422, {
2502
+ throw new HTTPException8(422, {
1857
2503
  message: "GitHub App repository resources require positive github_installation_id and github_repository_id"
1858
2504
  });
1859
2505
  }
@@ -1864,7 +2510,7 @@ function validateGitHubRepositorySelectionShape(resources) {
1864
2510
  }
1865
2511
  const installationId = selected[0].installationId;
1866
2512
  if (selected.some((item) => item.installationId !== installationId)) {
1867
- throw new HTTPException7(422, {
2513
+ throw new HTTPException8(422, {
1868
2514
  message: "GitHub App repository resources must belong to one installation"
1869
2515
  });
1870
2516
  }
@@ -1875,9 +2521,11 @@ async function validateGitHubRepositorySelection(db, workspaceId, resources) {
1875
2521
  if (installationId === null) {
1876
2522
  return;
1877
2523
  }
1878
- const linkedInstallationIds = new Set(await listGitHubInstallationIdsForWorkspace(db, workspaceId));
2524
+ const linkedInstallationIds = new Set(
2525
+ await listGitHubInstallationIdsForWorkspace(db, workspaceId)
2526
+ );
1879
2527
  if (!linkedInstallationIds.has(installationId)) {
1880
- throw new HTTPException7(422, {
2528
+ throw new HTTPException8(422, {
1881
2529
  message: "GitHub App repository resources must belong to a GitHub App installation linked to this workspace"
1882
2530
  });
1883
2531
  }
@@ -1889,22 +2537,24 @@ async function validateFileResources(db, workspaceId, resources) {
1889
2537
  continue;
1890
2538
  }
1891
2539
  if (fileIds.has(resource.fileId)) {
1892
- throw new HTTPException7(422, { message: `duplicate file resource: ${resource.fileId}` });
2540
+ throw new HTTPException8(422, { message: `duplicate file resource: ${resource.fileId}` });
1893
2541
  }
1894
2542
  fileIds.add(resource.fileId);
1895
2543
  const file = await requireFile(db, workspaceId, resource.fileId).catch(() => null);
1896
2544
  if (!file) {
1897
- throw new HTTPException7(422, { message: `unknown file resource: ${resource.fileId}` });
2545
+ throw new HTTPException8(422, { message: `unknown file resource: ${resource.fileId}` });
1898
2546
  }
1899
2547
  if (file.status !== "ready") {
1900
- 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
+ });
1901
2551
  }
1902
2552
  }
1903
2553
  }
1904
2554
  function normalizeMountPath(path) {
1905
2555
  const normalized = path.trim().replace(/^\/+|\/+$/g, "");
1906
2556
  if (!normalized || normalized.includes("..")) {
1907
- throw new HTTPException7(422, { message: `invalid resource mount path: ${path}` });
2557
+ throw new HTTPException8(422, { message: `invalid resource mount path: ${path}` });
1908
2558
  }
1909
2559
  return normalized;
1910
2560
  }
@@ -1912,7 +2562,7 @@ function parseResourceUrl(uri) {
1912
2562
  try {
1913
2563
  return new URL(uri);
1914
2564
  } catch {
1915
- throw new HTTPException7(422, { message: "repository resources must use valid URLs" });
2565
+ throw new HTTPException8(422, { message: "repository resources must use valid URLs" });
1916
2566
  }
1917
2567
  }
1918
2568
  function positiveInteger(value) {
@@ -1929,38 +2579,47 @@ function positiveInteger(value) {
1929
2579
  import {
1930
2580
  createScheduledTask,
1931
2581
  deleteScheduledTask,
2582
+ getRig as getRig3,
1932
2583
  getScheduledTask,
1933
2584
  updateScheduledTask
1934
2585
  } from "@opengeni/db";
1935
- import { HTTPException as HTTPException9 } from "hono/http-exception";
2586
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
1936
2587
 
1937
2588
  // src/domain/sessions.ts
1938
2589
  import { CODEX_MODEL_ID_PREFIX } from "@opengeni/codex";
1939
- import { configuredAllowedModels } from "@opengeni/config";
2590
+ import { configuredAllowedModels, policyProviderIdForModel } from "@opengeni/config";
1940
2591
  import {
1941
2592
  CreateSessionRequest,
2593
+ evaluateWorkspaceModelPolicy,
1942
2594
  reasoningEffortForMetadata
1943
2595
  } from "@opengeni/contracts";
1944
2596
  import {
1945
- appendSessionEventsWithLockedSessionUpdate,
1946
2597
  createSession,
1947
- createSessionGoal,
1948
2598
  createSessionWithIdempotencyKey,
1949
- enqueueSessionTurn,
1950
- encryptEnvironmentValue as encryptEnvironmentValue2,
2599
+ enqueueSessionMessageAtomically,
2600
+ encryptVariableSetValue as encryptVariableSetValue2,
1951
2601
  getAnySessionInGroup,
1952
2602
  getEnrollment as getEnrollment2,
1953
- listDistinctEnvironmentIdsInGroup,
2603
+ getRig as getRig2,
2604
+ getWorkspaceDefaultRigId,
2605
+ listDistinctVariableSetIdsInGroup,
2606
+ listDistinctRigVersionIdsInGroup,
1954
2607
  getSandbox as getSandbox3,
1955
2608
  getSession,
1956
2609
  getSessionByCreateIdempotencyKey,
2610
+ getSessionLineage,
1957
2611
  getSessionTurn,
2612
+ getWorkspaceModelPolicy,
2613
+ initializeSessionStartAtomically,
1958
2614
  requireSession as requireSession2,
1959
- setTemporalWorkflowId,
1960
- updateSessionTitle as updateSessionTitleRow
2615
+ updateSessionTitle as updateSessionTitleRow,
2616
+ SessionQueueConflictError
1961
2617
  } from "@opengeni/db";
1962
- import { appendAndPublishEvents } from "@opengeni/events";
1963
- 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";
1964
2623
  var reservedSessionMcpServerIds = /* @__PURE__ */ new Set(["opengeni", "files", "docs", "codex_apps"]);
1965
2624
  var maxSessionMcpCredentialHeaders = 16;
1966
2625
  var maxSessionMcpCredentialHeaderValueLength = 4096;
@@ -1971,23 +2630,29 @@ function normalizedSessionMcpCredentialHeaders(headers) {
1971
2630
  }
1972
2631
  const entries = Object.entries(headers).map(([name, value]) => [name.trim(), value]).filter(([name]) => name.length > 0);
1973
2632
  if (entries.length > maxSessionMcpCredentialHeaders) {
1974
- throw new 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
+ });
1975
2636
  }
1976
2637
  const seen = /* @__PURE__ */ new Set();
1977
2638
  for (const [name, value] of entries) {
1978
2639
  if (!sessionMcpCredentialHeaderName.test(name)) {
1979
- throw new HTTPException8(422, { message: `invalid credential header name: ${name}` });
2640
+ throw new HTTPException9(422, { message: `invalid credential header name: ${name}` });
1980
2641
  }
1981
2642
  const lower = name.toLowerCase();
1982
2643
  if (seen.has(lower)) {
1983
- throw new HTTPException8(422, { message: `duplicate credential header name: ${name}` });
2644
+ throw new HTTPException9(422, { message: `duplicate credential header name: ${name}` });
1984
2645
  }
1985
2646
  seen.add(lower);
1986
2647
  if (value.length === 0 || value.length > maxSessionMcpCredentialHeaderValueLength) {
1987
- throw new 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
+ });
1988
2651
  }
1989
2652
  if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
1990
- 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
+ });
1991
2656
  }
1992
2657
  }
1993
2658
  return Object.fromEntries(entries);
@@ -2018,10 +2683,7 @@ function settingsWithSessionMcpServerConfigs(settings, servers) {
2018
2683
  const sessionIds = new Set(servers.map((server) => server.id));
2019
2684
  return {
2020
2685
  ...settings,
2021
- mcpServers: [
2022
- ...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
2023
- ...servers
2024
- ]
2686
+ mcpServers: [...settings.mcpServers.filter((server) => !sessionIds.has(server.id)), ...servers]
2025
2687
  };
2026
2688
  }
2027
2689
  function settingsWithSessionMcpServerMetadata(settings, servers) {
@@ -2032,7 +2694,7 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2032
2694
  return { runtimeServers: [], dbServers: [], metadata: [] };
2033
2695
  }
2034
2696
  requirePermission(grant, "mcp_servers:attach");
2035
- const encryptionKey = requireEnvironmentEncryption(settings);
2697
+ const encryptionKey = requireVariableSetEncryption(settings);
2036
2698
  const existingIds = new Set(settings.mcpServers.map((server) => server.id));
2037
2699
  const seenIds = /* @__PURE__ */ new Set();
2038
2700
  const runtimeServers = [];
@@ -2040,15 +2702,18 @@ function validateSessionMcpServersForCreate(settings, grant, servers) {
2040
2702
  const metadata = [];
2041
2703
  for (const server of servers) {
2042
2704
  if (seenIds.has(server.id)) {
2043
- throw new HTTPException8(422, { message: `duplicate session MCP server id: ${server.id}` });
2705
+ throw new HTTPException9(422, { message: `duplicate session MCP server id: ${server.id}` });
2044
2706
  }
2045
2707
  seenIds.add(server.id);
2046
2708
  if (reservedSessionMcpServerIds.has(server.id) || existingIds.has(server.id)) {
2047
- throw new HTTPException8(422, { message: `MCP server id already exists: ${server.id}` });
2709
+ throw new HTTPException9(422, { message: `MCP server id already exists: ${server.id}` });
2048
2710
  }
2049
2711
  const headers = normalizedSessionMcpCredentialHeaders(server.headers);
2050
2712
  const headersEncrypted = Object.fromEntries(
2051
- Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue2(encryptionKey, value)])
2713
+ Object.entries(headers).map(([name, value]) => [
2714
+ name,
2715
+ encryptVariableSetValue2(encryptionKey, value)
2716
+ ])
2052
2717
  );
2053
2718
  runtimeServers.push(mcpServerConfigFromInput(server));
2054
2719
  dbServers.push({
@@ -2076,22 +2741,27 @@ function validateSessionMcpCredentialUpdates(input) {
2076
2741
  return [];
2077
2742
  }
2078
2743
  requirePermission(input.grant, "mcp_servers:attach");
2079
- const encryptionKey = requireEnvironmentEncryption(input.settings);
2744
+ const encryptionKey = requireVariableSetEncryption(input.settings);
2080
2745
  const knownIds = new Set(input.session.mcpServers.map((server) => server.id));
2081
2746
  const seenIds = /* @__PURE__ */ new Set();
2082
2747
  const encryptedUpdates = input.updates.map((update) => {
2083
2748
  if (seenIds.has(update.id)) {
2084
- throw new 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
+ });
2085
2752
  }
2086
2753
  seenIds.add(update.id);
2087
2754
  if (!knownIds.has(update.id)) {
2088
- 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}` });
2089
2756
  }
2090
2757
  const headers = normalizedSessionMcpCredentialHeaders(update.headers);
2091
2758
  return {
2092
2759
  id: update.id,
2093
2760
  headersEncrypted: Object.fromEntries(
2094
- Object.entries(headers).map(([name, value]) => [name, encryptEnvironmentValue2(encryptionKey, value)])
2761
+ Object.entries(headers).map(([name, value]) => [
2762
+ name,
2763
+ encryptVariableSetValue2(encryptionKey, value)
2764
+ ])
2095
2765
  )
2096
2766
  };
2097
2767
  });
@@ -2104,9 +2774,16 @@ async function createAndStartSession(input) {
2104
2774
  reasoningEffort: input.reasoningEffort
2105
2775
  };
2106
2776
  if (input.createIdempotencyKey) {
2107
- const existing = await getSessionByCreateIdempotencyKey(input.db, input.workspaceId, input.createIdempotencyKey);
2777
+ const existing = await getSessionByCreateIdempotencyKey(
2778
+ input.db,
2779
+ input.workspaceId,
2780
+ input.createIdempotencyKey
2781
+ );
2108
2782
  if (existing) {
2109
- return existing;
2783
+ return await finishStartSession(
2784
+ existing.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2785
+ existing
2786
+ );
2110
2787
  }
2111
2788
  const { session: keyed, created } = await createSessionWithIdempotencyKey(input.db, {
2112
2789
  accountId: input.accountId,
@@ -2117,7 +2794,9 @@ async function createAndStartSession(input) {
2117
2794
  metadata: sessionMetadata,
2118
2795
  model: input.model,
2119
2796
  sandboxBackend: input.sandboxBackend,
2120
- environmentId: input.environment?.id ?? null,
2797
+ variableSetId: input.variableSet?.id ?? null,
2798
+ rigId: input.rigId ?? null,
2799
+ rigVersionId: input.rigVersionId ?? null,
2121
2800
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
2122
2801
  instructions: input.instructions ?? null,
2123
2802
  parentSessionId: input.parentSessionId ?? null,
@@ -2127,7 +2806,10 @@ async function createAndStartSession(input) {
2127
2806
  mcpServers: input.mcpServers ?? []
2128
2807
  });
2129
2808
  if (!created) {
2130
- return keyed;
2809
+ return await finishStartSession(
2810
+ keyed.temporalWorkflowId ? { ...input, seedTargetSandbox: null } : input,
2811
+ keyed
2812
+ );
2131
2813
  }
2132
2814
  return await finishStartSession(input, keyed);
2133
2815
  }
@@ -2140,7 +2822,9 @@ async function createAndStartSession(input) {
2140
2822
  metadata: sessionMetadata,
2141
2823
  model: input.model,
2142
2824
  sandboxBackend: input.sandboxBackend,
2143
- environmentId: input.environment?.id ?? null,
2825
+ variableSetId: input.variableSet?.id ?? null,
2826
+ rigId: input.rigId ?? null,
2827
+ rigVersionId: input.rigVersionId ?? null,
2144
2828
  firstPartyMcpPermissions: input.firstPartyMcpPermissions ?? null,
2145
2829
  instructions: input.instructions ?? null,
2146
2830
  parentSessionId: input.parentSessionId ?? null,
@@ -2151,54 +2835,9 @@ async function createAndStartSession(input) {
2151
2835
  return await finishStartSession(input, session);
2152
2836
  }
2153
2837
  async function finishStartSession(input, session) {
2154
- const goal = input.goal ? await createSessionGoal(input.db, {
2155
- accountId: session.accountId,
2156
- workspaceId: session.workspaceId,
2157
- sessionId: session.id,
2158
- text: input.goal.text,
2159
- successCriteria: input.goal.successCriteria ?? null,
2160
- maxAutoContinuations: input.goal.maxAutoContinuations ?? null,
2161
- createdBy: "api"
2162
- }) : null;
2163
- const initialPayload = {
2164
- text: input.initialMessage,
2165
- ...input.resources.length ? { resources: input.resources } : {},
2166
- ...input.tools.length ? { tools: input.tools } : {}
2167
- };
2168
- const events = await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [
2169
- {
2170
- type: "session.created",
2171
- payload: {
2172
- status: "queued",
2173
- ...input.environment ? { environmentId: input.environment.id, environmentName: input.environment.name } : {},
2174
- ...input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}
2175
- }
2176
- },
2177
- ...goal ? [{
2178
- type: "goal.set",
2179
- payload: {
2180
- goalId: goal.id,
2181
- text: goal.text,
2182
- ...goal.successCriteria ? { successCriteria: goal.successCriteria } : {},
2183
- version: goal.version,
2184
- actor: "api",
2185
- replaced: false
2186
- }
2187
- }] : [],
2188
- {
2189
- type: "user.message",
2190
- payload: initialPayload,
2191
- ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2192
- },
2193
- { type: "session.status.changed", payload: { status: "queued" } }
2194
- ]);
2195
- const userEvent = events.find((event) => event.type === "user.message");
2196
- if (!userEvent) {
2197
- throw new HTTPException8(500, { message: "failed to append initial user event" });
2198
- }
2199
2838
  if (input.seedTargetSandbox) {
2200
2839
  if (session.sandboxBackend === "none") {
2201
- throw new HTTPException8(422, {
2840
+ throw new HTTPException9(422, {
2202
2841
  message: "cannot target a machine for a session with no sandbox (backend: none)"
2203
2842
  });
2204
2843
  }
@@ -2218,34 +2857,37 @@ async function finishStartSession(input, session) {
2218
2857
  input.seedTargetSandbox.workingDir ?? null
2219
2858
  );
2220
2859
  if (!seeded.swapped) {
2221
- throw new HTTPException8(422, {
2860
+ throw new HTTPException9(422, {
2222
2861
  message: `cannot target sandbox ${input.seedTargetSandbox.sandboxId}: ${seeded.reason ?? "target is not attachable"}`
2223
2862
  });
2224
2863
  }
2225
2864
  }
2226
- const workflowId = workflowIdForSession(session.id);
2227
- await setTemporalWorkflowId(input.db, session.workspaceId, session.id, workflowId);
2228
- const turn = await enqueueSessionTurn(input.db, {
2865
+ const started = await initializeSessionStartAtomically(input.db, {
2229
2866
  accountId: session.accountId,
2230
2867
  workspaceId: session.workspaceId,
2231
2868
  sessionId: session.id,
2232
- triggerEventId: userEvent.id,
2233
- temporalWorkflowId: workflowId,
2234
- source: "user",
2235
- prompt: input.initialMessage,
2236
- resources: input.resources,
2237
- tools: input.tools,
2238
- model: input.model,
2239
- reasoningEffort: input.reasoningEffort,
2240
- sandboxBackend: input.sandboxBackend,
2241
- 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
2242
2880
  });
2243
- await appendAndPublishEvents(input.db, input.bus, session.workspaceId, session.id, [{
2244
- type: "turn.queued",
2245
- turnId: turn.id,
2246
- payload: { turnId: turn.id, triggerEventId: userEvent.id, source: turn.source }
2247
- }]);
2248
- 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
+ }
2249
2891
  return await requireSession2(input.db, session.workspaceId, session.id);
2250
2892
  }
2251
2893
  function workflowIdForSession(sessionId) {
@@ -2261,15 +2903,33 @@ function assertConfiguredModel(settings, model) {
2261
2903
  if (settings.codexSubscriptionEnabled && model.startsWith(CODEX_MODEL_ID_PREFIX)) {
2262
2904
  return;
2263
2905
  }
2264
- throw new 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
+ }
2265
2923
  }
2266
2924
  async function requireQueuedTurnForApi(db, workspaceId, sessionId, turnId) {
2267
2925
  const turn = await getSessionTurn(db, workspaceId, turnId);
2268
2926
  if (!turn || turn.sessionId !== sessionId) {
2269
- throw new HTTPException8(404, { message: "session turn not found" });
2927
+ throw new HTTPException9(404, { message: "session turn not found" });
2270
2928
  }
2271
2929
  if (turn.status !== "queued") {
2272
- 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
+ });
2273
2933
  }
2274
2934
  return turn;
2275
2935
  }
@@ -2281,98 +2941,139 @@ async function postUserMessageTurn(input) {
2281
2941
  const requestedModel = input.model ?? null;
2282
2942
  const requestedReasoningEffort = input.reasoningEffort ?? null;
2283
2943
  assertConfiguredModel(settings, requestedModel);
2284
- const appended = await appendSessionEventsWithLockedSessionUpdate(db, workspaceId, sessionId, async (lockedSession, lockedUpdate) => {
2285
- if (lockedSession.status === "cancelled") {
2286
- 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 });
2287
2970
  }
2288
- const mcpCredentialUpdates = input.mcpCredentialUpdates?.length ? await lockedUpdate.updateSessionMcpServerCredentials(input.mcpCredentialUpdates) : { servers: [], missingIds: [] };
2289
- if (mcpCredentialUpdates.missingIds.length > 0) {
2290
- throw new HTTPException8(422, { message: `unknown session MCP server id: ${mcpCredentialUpdates.missingIds[0]}` });
2971
+ if (error instanceof Error && error.message.includes("cancelled")) {
2972
+ throw new HTTPException9(409, { message: error.message });
2291
2973
  }
2292
- const nextResources = mergeResourceRefs(lockedSession.resources, input.resources);
2293
- const nextTools = mergeToolRefs(lockedSession.tools, input.tools);
2294
- const shouldQueueSession = lockedSession.status === "idle" || lockedSession.status === "failed";
2295
- return {
2296
- events: [
2297
- {
2298
- type: "user.message",
2299
- payload: {
2300
- text: input.text,
2301
- ...input.resources.length ? { resources: input.resources } : {},
2302
- ...input.tools.length ? { tools: input.tools } : {},
2303
- ...requestedModel ? { model: requestedModel } : {},
2304
- ...requestedReasoningEffort ? { reasoningEffort: requestedReasoningEffort } : {},
2305
- ...mcpCredentialUpdates.servers.length ? { mcpCredentialUpdates: mcpCredentialUpdates.servers } : {}
2306
- },
2307
- ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2308
- },
2309
- ...shouldQueueSession ? [{ type: "session.status.changed", payload: { status: "queued" } }] : []
2310
- ],
2311
- update: {
2312
- resources: nextResources,
2313
- tools: nextTools,
2314
- ...shouldQueueSession ? { status: "queued", activeTurnId: null } : {}
2315
- }
2316
- };
2317
- }).then(async (events) => {
2318
- await bus.publish(workspaceId, sessionId, events);
2319
- return events;
2320
- });
2321
- const accepted = appended[0];
2322
- if (!accepted) {
2323
- throw new HTTPException8(500, { message: "failed to append client event" });
2324
- }
2325
- const workflowId = workflowIdForSession(sessionId);
2326
- const session = await requireSession2(db, workspaceId, sessionId);
2327
- const turn = await enqueueSessionTurn(db, {
2328
- accountId,
2329
- workspaceId,
2330
- sessionId,
2331
- triggerEventId: accepted.id,
2332
- temporalWorkflowId: workflowId,
2333
- source: "user",
2334
- prompt: input.text,
2335
- resources: input.resources,
2336
- tools: input.tools,
2337
- model: requestedModel ?? session.model,
2338
- reasoningEffort: requestedReasoningEffort ?? reasoningEffortForSession(session.metadata, settings.openaiReasoningEffort),
2339
- sandboxBackend: session.sandboxBackend,
2340
- metadata: {}
2341
- });
2342
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
2343
- type: "turn.queued",
2344
- turnId: turn.id,
2345
- payload: { turnId: turn.id, triggerEventId: accepted.id, source: turn.source }
2346
- }]);
2347
- await workflowClient.wakeSessionWorkflow({ accountId, workspaceId, sessionId, workflowId });
2348
- return { accepted, turn };
2974
+ if (error instanceof Error && error.message.startsWith("Unknown session MCP server")) {
2975
+ throw new HTTPException9(422, { message: error.message });
2976
+ }
2977
+ throw error;
2978
+ }
2979
+ await bus.publish(workspaceId, sessionId, result.events);
2980
+ if (result.shouldSignalControl && result.controlEvent) {
2981
+ if (result.workflowWakeRevision === null) {
2982
+ throw new Error("Steer control has no workflow wake revision");
2983
+ }
2984
+ await workflowClient.signalSessionControl({
2985
+ accountId,
2986
+ workspaceId,
2987
+ sessionId,
2988
+ eventId: result.controlEvent.id,
2989
+ workflowId: result.temporalWorkflowId,
2990
+ workflowWakeRevision: result.workflowWakeRevision
2991
+ });
2992
+ } else if (result.shouldWake) {
2993
+ if (result.workflowWakeRevision === null) {
2994
+ throw new Error("Runnable prompt has no workflow wake revision");
2995
+ }
2996
+ await workflowClient.wakeSessionWorkflow({
2997
+ accountId,
2998
+ workspaceId,
2999
+ sessionId,
3000
+ workflowId: result.temporalWorkflowId,
3001
+ wakeRevision: result.workflowWakeRevision
3002
+ });
3003
+ }
3004
+ return {
3005
+ accepted: result.accepted,
3006
+ turn: result.turn
3007
+ };
2349
3008
  }
2350
3009
  async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2351
3010
  const { settings, db, bus, workflowClient, objectStorage } = deps;
2352
3011
  const payload = CreateSessionRequest.parse(rawPayload);
2353
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
2354
- const sessionMcpServers = validateSessionMcpServersForCreate(capabilityRuntimeSettings, grant, payload.mcpServers);
2355
- 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
+ );
2356
3026
  const resources = normalizeResources(payload.resources);
2357
3027
  const requestedTools = validateToolRefs(payload.tools, runtimeSettings);
2358
3028
  const defaultedTools = hasOwnProperty(rawPayload, "tools") ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, settings, capabilityRuntimeSettings);
2359
3029
  const tools = withFirstPartyTools(defaultedTools, runtimeSettings);
2360
3030
  await validateGitHubRepositorySelection(db, workspaceId, resources);
2361
3031
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
2362
- throw new HTTPException8(503, { message: "object storage is not configured" });
3032
+ throw new HTTPException9(503, { message: "object storage is not configured" });
2363
3033
  }
2364
3034
  await validateFileResources(db, workspaceId, resources);
2365
- 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
+ }
2366
3057
  assertConfiguredModel(settings, payload.model);
3058
+ await assertWorkspaceModelPolicyAllows(
3059
+ db,
3060
+ settings,
3061
+ workspaceId,
3062
+ payload.model ?? settings.openaiModel
3063
+ );
2367
3064
  const model = payload.model ?? settings.openaiModel;
2368
3065
  const reasoningEffort = payload.reasoningEffort ?? settings.openaiReasoningEffort;
2369
3066
  let firstPartyMcpPermissions = payload.firstPartyMcpPermissions ?? null;
2370
3067
  if (firstPartyMcpPermissions && firstPartyMcpPermissions.length === 0) {
2371
- throw new 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
+ });
2372
3071
  }
2373
3072
  for (const permission of firstPartyMcpPermissions ?? []) {
2374
3073
  if (!hasPermission(grant.permissions, permission)) {
2375
- 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
+ });
2376
3077
  }
2377
3078
  }
2378
3079
  if (payload.goal && firstPartyMcpPermissions && !firstPartyMcpPermissions.includes("goals:manage")) {
@@ -2382,19 +3083,39 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2382
3083
  const sandboxChoice = payload.sandbox ?? (parentSessionId ? "shared" : "new");
2383
3084
  let sandboxGroupId = null;
2384
3085
  let inheritedBackend;
2385
- const requestedEnvironmentId = payload.environmentId ?? null;
2386
- const environmentMatchesGroup = (memberEnvironmentId) => memberEnvironmentId === requestedEnvironmentId;
3086
+ const requestedVariableSetId = payload.variableSetId ?? null;
3087
+ const variableSetMatchesGroup = (memberVariableSetId) => memberVariableSetId === requestedVariableSetId;
3088
+ const rigVersionMatchesGroup = (memberRigVersionId) => memberRigVersionId === frozenRigVersionId;
2387
3089
  if (sandboxChoice === "shared") {
2388
3090
  if (!parentSessionId) {
2389
- throw new 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
+ });
2390
3094
  }
2391
3095
  const parent = await getSession(db, workspaceId, parentSessionId);
2392
3096
  if (!parent) {
2393
- 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
+ });
2394
3100
  }
2395
- 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) {
2396
3115
  if (payload.sandbox === "shared") {
2397
- 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
+ });
2398
3119
  }
2399
3120
  } else {
2400
3121
  sandboxGroupId = parent.sandboxGroupId;
@@ -2403,19 +3124,43 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2403
3124
  } else if (typeof sandboxChoice === "object") {
2404
3125
  const member = await getAnySessionInGroup(db, workspaceId, sandboxChoice.groupId);
2405
3126
  if (!member) {
2406
- throw new 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
+ });
2407
3130
  }
2408
3131
  if (member.sandboxBackend !== "none") {
2409
- const memberEnvironmentIds = await listDistinctEnvironmentIdsInGroup(db, workspaceId, sandboxChoice.groupId);
2410
- if (!memberEnvironmentIds.every((memberEnvironmentId) => environmentMatchesGroup(memberEnvironmentId))) {
2411
- 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
+ });
2412
3155
  }
2413
3156
  }
2414
3157
  sandboxGroupId = sandboxChoice.groupId;
2415
3158
  inheritedBackend = member.sandboxBackend;
2416
3159
  }
2417
3160
  if (payload.workingDir !== void 0 && !payload.targetSandboxId) {
2418
- throw new 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
+ });
2419
3164
  }
2420
3165
  let machineHomeBackend;
2421
3166
  let machineHomeOs;
@@ -2431,7 +3176,13 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2431
3176
  }
2432
3177
  }
2433
3178
  }
2434
- await requireLimit(deps, { 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
+ });
2435
3186
  const session = await createAndStartSession({
2436
3187
  db,
2437
3188
  bus,
@@ -2456,7 +3207,10 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2456
3207
  ...machineHomeOs ? { sandboxOs: machineHomeOs } : {},
2457
3208
  sandboxGroupId,
2458
3209
  metadata: payload.metadata,
2459
- 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,
2460
3214
  goal: payload.goal ?? null,
2461
3215
  // Per-session persona instructions (already trimmed/validated by the
2462
3216
  // contracts schema). Persisted on the row; composed system-level at turn
@@ -2488,9 +3242,16 @@ async function createSessionForRequest(deps, grant, workspaceId, rawPayload) {
2488
3242
  }
2489
3243
  async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, input) {
2490
3244
  const { settings, db, bus, workflowClient, objectStorage } = deps;
2491
- const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
3245
+ const capabilityRuntimeSettings = await settingsWithEnabledCapabilityMcpServers(
3246
+ db,
3247
+ workspaceId,
3248
+ settings
3249
+ );
2492
3250
  const existingSession = await requireSession2(db, workspaceId, sessionId);
2493
- const runtimeSettings = settingsWithSessionMcpServerMetadata(capabilityRuntimeSettings, existingSession.mcpServers);
3251
+ const runtimeSettings = settingsWithSessionMcpServerMetadata(
3252
+ capabilityRuntimeSettings,
3253
+ existingSession.mcpServers
3254
+ );
2494
3255
  const requestedResources = normalizeResources(input.resources ?? []);
2495
3256
  const validatedTools = validateToolRefs(input.tools ?? [], runtimeSettings);
2496
3257
  const requestedTools = input.toolsProvided ? validatedTools : withDefaultEnabledCapabilityMcpTools(validatedTools, settings, capabilityRuntimeSettings);
@@ -2502,10 +3263,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
2502
3263
  model: input.model ?? existingSession.model
2503
3264
  });
2504
3265
  if (requestedResources.some((resource) => resource.kind === "file") && !objectStorage) {
2505
- throw new HTTPException8(503, { message: "object storage is not configured" });
3266
+ throw new HTTPException9(503, { message: "object storage is not configured" });
2506
3267
  }
2507
3268
  await validateFileResources(db, workspaceId, requestedResources);
2508
- await validateGitHubRepositorySelection(db, workspaceId, [...existingSession.resources, ...requestedResources]);
3269
+ await validateGitHubRepositorySelection(db, workspaceId, [
3270
+ ...existingSession.resources,
3271
+ ...requestedResources
3272
+ ]);
2509
3273
  const mcpCredentialUpdates = validateSessionMcpCredentialUpdates({
2510
3274
  settings,
2511
3275
  grant,
@@ -2526,6 +3290,13 @@ async function acceptSessionUserMessage(deps, grant, workspaceId, sessionId, inp
2526
3290
  model: input.model ?? null,
2527
3291
  reasoningEffort: input.reasoningEffort ?? null,
2528
3292
  mcpCredentialUpdates,
3293
+ delivery: input.delivery ?? "queue",
3294
+ origin: input.origin ?? "human",
3295
+ actor: grant.subjectId,
3296
+ ...input.expectedControlGeneration !== void 0 ? { expectedControlGeneration: input.expectedControlGeneration } : {},
3297
+ ...input.expectedWorkspaceInferenceGeneration !== void 0 ? {
3298
+ expectedWorkspaceInferenceGeneration: input.expectedWorkspaceInferenceGeneration
3299
+ } : {},
2529
3300
  ...input.clientEventId ? { clientEventId: input.clientEventId } : {}
2530
3301
  });
2531
3302
  await recordWorkspaceUsage(deps, {
@@ -2545,16 +3316,25 @@ async function updateSessionTitle(deps, workspaceId, sessionId, title, source) {
2545
3316
  const { db, bus } = deps;
2546
3317
  const result = await updateSessionTitleRow(db, { workspaceId, sessionId, title, source });
2547
3318
  if (result.updated) {
2548
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [{
2549
- type: "session.title_set",
2550
- payload: {
2551
- title: result.title ?? title,
2552
- source
3319
+ await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
3320
+ {
3321
+ type: "session.title_set",
3322
+ payload: {
3323
+ title: result.title ?? title,
3324
+ source
3325
+ }
2553
3326
  }
2554
- }]);
3327
+ ]);
2555
3328
  }
2556
3329
  return result;
2557
3330
  }
3331
+ async function readSessionLineage(db, workspaceId, sessionId) {
3332
+ const lineage = await getSessionLineage(db, workspaceId, sessionId);
3333
+ if (!lineage) {
3334
+ throw new HTTPException9(404, { message: "session not found" });
3335
+ }
3336
+ return lineage;
3337
+ }
2558
3338
  function withFirstPartyTools(tools, runtimeSettings) {
2559
3339
  if (!runtimeSettings.mcpServers.some((server) => server.id === "opengeni")) {
2560
3340
  return tools;
@@ -2562,7 +3342,9 @@ function withFirstPartyTools(tools, runtimeSettings) {
2562
3342
  return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
2563
3343
  }
2564
3344
  function hasOwnProperty(value, key) {
2565
- return Boolean(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
+ );
2566
3348
  }
2567
3349
 
2568
3350
  // src/domain/scheduled-tasks.ts
@@ -2576,18 +3358,24 @@ function scheduledTaskToolsProvided(rawPayload) {
2576
3358
  );
2577
3359
  }
2578
3360
  async function createValidatedScheduledTask(input) {
2579
- const agentConfig = await validateScheduledTaskAgentConfig({ ...input, workspaceId: input.grant.workspaceId });
3361
+ const agentConfig = await validateScheduledTaskAgentConfig({
3362
+ ...input,
3363
+ workspaceId: input.grant.workspaceId
3364
+ });
2580
3365
  const id = crypto.randomUUID();
2581
3366
  validateScheduledTaskSchedule(input.payload.schedule);
2582
- if (input.payload.environmentId) {
2583
- await validateEnvironmentAttachment(
3367
+ if (input.payload.variableSetId) {
3368
+ await validateVariableSetAttachment(
2584
3369
  { settings: input.settings, db: input.db },
2585
3370
  input.grant,
2586
3371
  input.grant.workspaceId,
2587
- input.payload.environmentId,
2588
- { preauthorized: input.environmentPreauthorized ?? false }
3372
+ input.payload.variableSetId,
3373
+ { preauthorized: input.variableSetPreauthorized ?? false }
2589
3374
  );
2590
3375
  }
3376
+ if (input.payload.rigId) {
3377
+ await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
3378
+ }
2591
3379
  return await createScheduledTask(input.db, {
2592
3380
  id,
2593
3381
  accountId: input.grant.accountId,
@@ -2599,10 +3387,17 @@ async function createValidatedScheduledTask(input) {
2599
3387
  runMode: input.payload.runMode,
2600
3388
  overlapPolicy: input.payload.overlapPolicy,
2601
3389
  agentConfig,
2602
- environmentId: input.payload.environmentId ?? null,
3390
+ variableSetId: input.payload.variableSetId ?? null,
3391
+ rigId: input.payload.rigId ?? null,
2603
3392
  metadata: input.payload.metadata
2604
3393
  });
2605
3394
  }
3395
+ async function requireScheduledTaskRig(db, workspaceId, rigId) {
3396
+ const rig = await getRig3(db, workspaceId, rigId);
3397
+ if (!rig) {
3398
+ throw new HTTPException10(422, { message: `unknown rigId: ${rigId}` });
3399
+ }
3400
+ }
2606
3401
  async function validatedScheduledTaskUpdate(input) {
2607
3402
  const update = {};
2608
3403
  if (input.payload.name !== void 0) {
@@ -2624,30 +3419,38 @@ async function validatedScheduledTaskUpdate(input) {
2624
3419
  if (input.payload.metadata !== void 0) {
2625
3420
  update.metadata = input.payload.metadata;
2626
3421
  }
2627
- if (input.payload.environmentId !== void 0) {
2628
- const nextEnvironmentId = input.payload.environmentId;
2629
- if ((input.existing.environmentId ?? null) !== (nextEnvironmentId ?? null) && input.existing.runMode === "reusable_session" && input.existing.reusableSessionId) {
2630
- 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
+ });
2631
3428
  }
2632
- if (nextEnvironmentId === null) {
2633
- if (input.existing.environmentId !== null) {
2634
- requirePermission(input.grant, "environments:use");
3429
+ if (nextVariableSetId === null) {
3430
+ if (input.existing.variableSetId !== null) {
3431
+ requirePermission(input.grant, "variable-sets:use");
2635
3432
  }
2636
- update.environmentId = null;
3433
+ update.variableSetId = null;
2637
3434
  } else {
2638
- await validateEnvironmentAttachment(
3435
+ await validateVariableSetAttachment(
2639
3436
  { settings: input.settings, db: input.db },
2640
3437
  input.grant,
2641
3438
  input.existing.workspaceId,
2642
- nextEnvironmentId
3439
+ nextVariableSetId
2643
3440
  );
2644
- update.environmentId = nextEnvironmentId;
3441
+ update.variableSetId = nextVariableSetId;
2645
3442
  }
2646
3443
  }
3444
+ if (input.payload.rigId !== void 0) {
3445
+ if (input.payload.rigId !== null) {
3446
+ await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
3447
+ }
3448
+ update.rigId = input.payload.rigId;
3449
+ }
2647
3450
  if (input.payload.agentConfig !== void 0) {
2648
- const willHaveEnvironment = input.payload.environmentId !== void 0 ? input.payload.environmentId !== null : Boolean(input.existing.environmentId);
2649
- if (willHaveEnvironment) {
2650
- 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");
2651
3454
  }
2652
3455
  update.agentConfig = await validateScheduledTaskAgentConfig({
2653
3456
  settings: input.settings,
@@ -2663,7 +3466,7 @@ async function validatedScheduledTaskUpdate(input) {
2663
3466
  async function requireScheduledTaskForApi(db, workspaceId, taskId) {
2664
3467
  const task = await getScheduledTask(db, workspaceId, taskId);
2665
3468
  if (!task) {
2666
- throw new HTTPException9(404, { message: "scheduled task not found" });
3469
+ throw new HTTPException10(404, { message: "scheduled task not found" });
2667
3470
  }
2668
3471
  return task;
2669
3472
  }
@@ -2676,7 +3479,7 @@ async function restoreScheduledTask(db, task) {
2676
3479
  overlapPolicy: task.overlapPolicy,
2677
3480
  agentConfig: task.agentConfig,
2678
3481
  reusableSessionId: task.reusableSessionId,
2679
- environmentId: task.environmentId,
3482
+ variableSetId: task.variableSetId,
2680
3483
  metadata: task.metadata
2681
3484
  });
2682
3485
  }
@@ -2684,7 +3487,9 @@ async function syncCreatedScheduledTask(input) {
2684
3487
  try {
2685
3488
  await input.workflowClient.syncScheduledTask({ task: input.task });
2686
3489
  } catch (error) {
2687
- await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(() => void 0);
3490
+ await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
3491
+ () => void 0
3492
+ );
2688
3493
  throw error;
2689
3494
  }
2690
3495
  }
@@ -2715,17 +3520,27 @@ function manualScheduledTaskTriggerUsageKey(workspaceId, taskId, triggerToken) {
2715
3520
  }
2716
3521
  async function validateScheduledTaskAgentConfig(input) {
2717
3522
  assertConfiguredModel(input.settings, input.payload.agentConfig.model);
3523
+ await assertWorkspaceModelPolicyAllows(
3524
+ input.db,
3525
+ input.settings,
3526
+ input.workspaceId,
3527
+ input.payload.agentConfig.model
3528
+ );
2718
3529
  const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
2719
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(input.db, input.workspaceId, input.settings);
3530
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
3531
+ input.db,
3532
+ input.workspaceId,
3533
+ input.settings
3534
+ );
2720
3535
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
2721
3536
  const tools = input.toolsProvided ?? true ? requestedTools : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
2722
3537
  const prompt = input.payload.agentConfig.prompt.trim();
2723
3538
  if (!prompt) {
2724
- throw new HTTPException9(422, { message: "scheduled task prompt is required" });
3539
+ throw new HTTPException10(422, { message: "scheduled task prompt is required" });
2725
3540
  }
2726
3541
  await validateGitHubRepositorySelection(input.db, input.workspaceId, resources);
2727
3542
  if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
2728
- throw new HTTPException9(503, { message: "object storage is not configured" });
3543
+ throw new HTTPException10(503, { message: "object storage is not configured" });
2729
3544
  }
2730
3545
  await validateFileResources(input.db, input.workspaceId, resources);
2731
3546
  return {
@@ -2740,19 +3555,19 @@ function validateScheduledTaskSchedule(schedule) {
2740
3555
  return;
2741
3556
  }
2742
3557
  if (new Date(schedule.startAt).getTime() >= new Date(schedule.endAt).getTime()) {
2743
- throw new HTTPException9(422, { message: "interval schedule endAt must be after startAt" });
3558
+ throw new HTTPException10(422, { message: "interval schedule endAt must be after startAt" });
2744
3559
  }
2745
3560
  }
2746
3561
  function trimmedScheduledTaskName(name) {
2747
3562
  const trimmed = name.trim();
2748
3563
  if (!trimmed) {
2749
- throw new HTTPException9(422, { message: "scheduled task name is required" });
3564
+ throw new HTTPException10(422, { message: "scheduled task name is required" });
2750
3565
  }
2751
3566
  return trimmed;
2752
3567
  }
2753
3568
 
2754
3569
  // src/domain/workspace-members.ts
2755
- import { HTTPException as HTTPException10 } from "hono/http-exception";
3570
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
2756
3571
  var MEMBER_ADMIN_PERMISSIONS = ["workspace:admin", "members:manage"];
2757
3572
  function memberCanAdminister(member) {
2758
3573
  return member.permissions.some((permission) => MEMBER_ADMIN_PERMISSIONS.includes(permission));
@@ -2762,55 +3577,71 @@ function isUserMember(member) {
2762
3577
  }
2763
3578
  function resolveMemberSubjectId(userId) {
2764
3579
  if (!userId) {
2765
- throw new HTTPException10(404, { message: "user is not registered" });
3580
+ throw new HTTPException11(404, { message: "user is not registered" });
2766
3581
  }
2767
3582
  return `user:${userId}`;
2768
3583
  }
2769
3584
  function assertWorkspaceMemberRemovable(input) {
2770
3585
  const { members, subjectId, callerSubjectId } = input;
2771
3586
  if (subjectId === callerSubjectId) {
2772
- throw new HTTPException10(409, { message: "you cannot remove your own membership" });
3587
+ throw new HTTPException11(409, { message: "you cannot remove your own membership" });
2773
3588
  }
2774
3589
  const target = members.find((member) => member.subjectId === subjectId);
2775
3590
  if (!target) {
2776
- throw new HTTPException10(404, { message: "member not found" });
3591
+ throw new HTTPException11(404, { message: "member not found" });
2777
3592
  }
2778
3593
  if (memberCanAdminister(target)) {
2779
- const remainingAdmins = members.filter((member) => member.subjectId !== subjectId && memberCanAdminister(member));
3594
+ const remainingAdmins = members.filter(
3595
+ (member) => member.subjectId !== subjectId && memberCanAdminister(member)
3596
+ );
2780
3597
  if (remainingAdmins.length === 0) {
2781
- 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
+ });
2782
3601
  }
2783
3602
  }
2784
3603
  }
2785
3604
  function assertWorkspaceDeletable(input) {
2786
3605
  if (input.workspaceCountForAccount <= 1) {
2787
- 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" });
2788
3607
  }
2789
3608
  if (input.activeSessionCount > 0) {
2790
- throw new HTTPException10(409, {
3609
+ throw new HTTPException11(409, {
2791
3610
  message: "stop the workspace's running sessions before deleting it"
2792
3611
  });
2793
3612
  }
2794
3613
  }
2795
3614
  export {
2796
3615
  MARKETING_SOCIAL_PACK_ID,
3616
+ MAX_CHECKS_PER_RIG,
3617
+ MAX_CREDENTIAL_HOOKS_PER_RIG,
3618
+ MAX_DEFAULT_VARIABLE_SETS_PER_RIG,
2797
3619
  MAX_ENVIRONMENTS_PER_WORKSPACE,
3620
+ MAX_RIGS_PER_WORKSPACE,
2798
3621
  MAX_VARIABLES_PER_ENVIRONMENT,
2799
3622
  acceptSessionUserMessage,
3623
+ activateRigVersionForApi,
3624
+ appendRigSetupCommand,
2800
3625
  applyCapabilityEnablement,
2801
3626
  assertAllowedEnvironmentVariableName,
3627
+ assertAllowedVariableSetVariableName,
2802
3628
  assertConfiguredModel,
2803
3629
  assertPackSandboxImageCompatible,
2804
3630
  assertWorkspaceDeletable,
2805
3631
  assertWorkspaceMemberRemovable,
3632
+ assertWorkspaceModelPolicyAllows,
2806
3633
  buildCapabilityCatalog,
2807
3634
  buildFleetContextForSession,
2808
3635
  buildMarketingDailyAnalysisAgentConfig,
2809
3636
  checkLimit,
3637
+ classifyRigVerificationOutcome,
2810
3638
  createAndStartSession,
2811
3639
  createCatalogItem,
3640
+ createRigForApi,
3641
+ createRigVersionForApi,
2812
3642
  createSessionForRequest,
2813
3643
  createValidatedScheduledTask,
3644
+ deleteRigForApi,
2814
3645
  disableCapability,
2815
3646
  discoverMcpRegistryCapabilities,
2816
3647
  enableCapability,
@@ -2821,6 +3652,8 @@ export {
2821
3652
  isUserMember,
2822
3653
  listCapabilityPacks,
2823
3654
  listFleet,
3655
+ listRigChangesForApi,
3656
+ listRigVersionsForApi,
2824
3657
  listWorkspaceCapabilityPacks,
2825
3658
  manualScheduledTaskTriggerUsageKey,
2826
3659
  manualScheduledTaskTriggerWorkflowId,
@@ -2830,23 +3663,32 @@ export {
2830
3663
  normalizeResources,
2831
3664
  officialMcpRegistryUrl,
2832
3665
  postUserMessageTurn,
3666
+ promoteSetupAppendChange,
3667
+ promoteVerifiedDefinitionEditChangeForApi,
3668
+ proposeRigChangeForApi,
2833
3669
  provisionSandbox,
3670
+ readSessionLineage,
2834
3671
  reasoningEffortForSession,
2835
- recordEnvironmentAuditEvent,
3672
+ recordRigAuditEvent,
3673
+ recordVariableSetAuditEvent,
2836
3674
  recordWorkspaceUsage,
2837
3675
  relayConfigFromSettings,
2838
3676
  relayDialBaseFromSettings,
2839
3677
  requireAccessContext,
2840
3678
  requireAccessGrant,
2841
3679
  requireEnvironmentEncryption,
2842
- requireEnvironmentForApi,
2843
3680
  requireLimit,
2844
3681
  requirePermission,
2845
3682
  requireQueuedTurnForApi,
3683
+ requireRigChangeForApi,
3684
+ requireRigForApi,
2846
3685
  requireScheduledTaskForApi,
3686
+ requireVariableSetEncryption,
3687
+ requireVariableSetForApi,
2847
3688
  resolveCapabilityPack,
2848
3689
  resolveMemberSubjectId,
2849
3690
  restoreScheduledTask,
3691
+ rigActorForGrant,
2850
3692
  routingEnabled,
2851
3693
  runOnSandbox,
2852
3694
  scheduledTaskTemporalScheduleId,
@@ -2859,13 +3701,14 @@ export {
2859
3701
  swapActiveSandbox,
2860
3702
  syncCreatedScheduledTask,
2861
3703
  syncUpdatedScheduledTask,
3704
+ updateRigForApi,
2862
3705
  updateSessionTitle,
2863
- validateEnvironmentAttachment,
2864
3706
  validateFileResources,
2865
3707
  validateGitHubRepositorySelection,
2866
3708
  validateGitHubRepositorySelectionShape,
2867
3709
  validateMcpCapabilityConnection,
2868
3710
  validateToolRefs,
3711
+ validateVariableSetAttachment,
2869
3712
  validatedScheduledTaskUpdate,
2870
3713
  withDefaultEnabledCapabilityMcpTools,
2871
3714
  workflowIdForSession,