@opengeni/core 0.4.6 → 0.4.8

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