@opengeni/core 0.28.1 → 1.0.0

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.
Files changed (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18812 -6833
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,101 @@
1
+ import { SESSION_TURN_SURFACES, type SessionTurnSurface } from "@opengeni/contracts";
2
+ import type { Observability } from "@opengeni/observability";
3
+
4
+ /**
5
+ * Live, content-free product usage counters recorded where work is accepted.
6
+ * Every label is a closed set: the product surface (or `unknown` when no
7
+ * surface was captured), who created a session (`subject` or `service`), and
8
+ * whether the session is a root. No workspace, user, model or text value ever
9
+ * becomes a label.
10
+ */
11
+ export type ProductUsageMetricsSink = Pick<Observability, "incrementCounter">;
12
+
13
+ export const PRODUCT_USAGE_SURFACE_LABELS = [...SESSION_TURN_SURFACES, "unknown"] as const;
14
+ export type ProductUsageSurfaceLabel = (typeof PRODUCT_USAGE_SURFACE_LABELS)[number];
15
+ export const SESSION_CREATED_BY_KINDS = ["subject", "service"] as const;
16
+ export type SessionCreatedByKind = (typeof SESSION_CREATED_BY_KINDS)[number];
17
+
18
+ const SESSIONS_CREATED_METRIC = {
19
+ name: "opengeni_sessions_created_total",
20
+ help: "Sessions created by product surface, creator kind, and whether they are root sessions.",
21
+ } as const;
22
+ const USER_MESSAGES_METRIC = {
23
+ name: "opengeni_user_messages_total",
24
+ help: "Accepted user messages and scheduled prompts by product surface.",
25
+ } as const;
26
+
27
+ export function productUsageSurfaceLabel(
28
+ surface: SessionTurnSurface | null | undefined,
29
+ ): ProductUsageSurfaceLabel {
30
+ return surface && (SESSION_TURN_SURFACES as readonly string[]).includes(surface)
31
+ ? surface
32
+ : "unknown";
33
+ }
34
+
35
+ function sessionCreatedByKind(kind: string | null | undefined): SessionCreatedByKind {
36
+ return kind === "subject" ? "subject" : "service";
37
+ }
38
+
39
+ /** Count one newly created session. Never throws; replays must not call it. */
40
+ export function recordSessionCreated(
41
+ sink: ProductUsageMetricsSink | null | undefined,
42
+ input: {
43
+ surface: SessionTurnSurface | null | undefined;
44
+ createdByKind: string | null | undefined;
45
+ parentSessionId: string | null | undefined;
46
+ },
47
+ ): void {
48
+ try {
49
+ sink?.incrementCounter({
50
+ ...SESSIONS_CREATED_METRIC,
51
+ labels: {
52
+ surface: productUsageSurfaceLabel(input.surface),
53
+ created_by_kind: sessionCreatedByKind(input.createdByKind),
54
+ root: input.parentSessionId ? "false" : "true",
55
+ },
56
+ });
57
+ } catch {
58
+ // Telemetry must never change the create outcome.
59
+ }
60
+ }
61
+
62
+ /** Count one newly accepted user message. Never throws; replays must not call it. */
63
+ export function recordUserMessageAccepted(
64
+ sink: ProductUsageMetricsSink | null | undefined,
65
+ input: { surface: SessionTurnSurface | null | undefined },
66
+ ): void {
67
+ try {
68
+ sink?.incrementCounter({
69
+ ...USER_MESSAGES_METRIC,
70
+ labels: { surface: productUsageSurfaceLabel(input.surface) },
71
+ });
72
+ } catch {
73
+ // Telemetry must never change the send outcome.
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Publish every closed series at zero so dashboards can tell a quiet surface
79
+ * from missing instrumentation. Never throws.
80
+ */
81
+ export function registerProductUsageMetricBaselines(
82
+ sink: ProductUsageMetricsSink | null | undefined,
83
+ ): void {
84
+ if (!sink) return;
85
+ try {
86
+ for (const surface of PRODUCT_USAGE_SURFACE_LABELS) {
87
+ sink.incrementCounter({ ...USER_MESSAGES_METRIC, labels: { surface }, amount: 0 });
88
+ for (const createdByKind of SESSION_CREATED_BY_KINDS) {
89
+ for (const root of ["true", "false"]) {
90
+ sink.incrementCounter({
91
+ ...SESSIONS_CREATED_METRIC,
92
+ labels: { surface, created_by_kind: createdByKind, root },
93
+ amount: 0,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ } catch {
99
+ // Telemetry only; series appear on their first real increment.
100
+ }
101
+ }
package/src/rigs/index.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  ProposeRigChangeRequest,
12
12
  Rig,
13
13
  RigChange,
14
+ ResourceAuthorityScope,
14
15
  RigVersion,
15
16
  UpdateRigRequest,
16
17
  } from "@opengeni/contracts";
@@ -21,7 +22,6 @@ import {
21
22
  createRigChange,
22
23
  createRigVersion,
23
24
  createRigVersionForChangePromotion,
24
- deleteRigIfNoActiveSessions,
25
25
  getRig,
26
26
  getRigByName,
27
27
  getRigChange,
@@ -30,12 +30,15 @@ import {
30
30
  listRigChanges,
31
31
  listRigVersions,
32
32
  recordAuditEvent,
33
+ revokeScopedRig,
33
34
  RigActiveVersionChangedError,
34
35
  RigChangeTransitionError,
35
- updateRig,
36
+ updateScopedRig,
36
37
  type Database,
37
38
  } from "@opengeni/db";
38
39
  import { HTTPException } from "hono/http-exception";
40
+ import { boundedParallelMap } from "@opengeni/runtime/mcp-network";
41
+ import { requirePermission } from "../access";
39
42
  import { rigProviderImagesFromVerification } from "./provider-images";
40
43
 
41
44
  export * from "./provider-images";
@@ -44,6 +47,34 @@ export const MAX_RIGS_PER_WORKSPACE = 50;
44
47
  export const MAX_CHECKS_PER_RIG = 100;
45
48
  export const MAX_CREDENTIAL_HOOKS_PER_RIG = 50;
46
49
  export const MAX_DEFAULT_VARIABLE_SETS_PER_RIG = 25;
50
+ export const RIG_DEFAULT_VARIABLE_SET_LOAD_CONCURRENCY = 4;
51
+
52
+ type VariableSetEnvironment = { values: Record<string, string> } | null;
53
+
54
+ /** Load complete rig defaults concurrently while preserving listed precedence. */
55
+ export async function loadRigDefaultVariableSetEnvironment(
56
+ variableSetIds: readonly string[],
57
+ load: (variableSetId: string) => Promise<VariableSetEnvironment>,
58
+ ): Promise<Record<string, string>> {
59
+ const variableSets = await boundedParallelMap(
60
+ variableSetIds,
61
+ RIG_DEFAULT_VARIABLE_SET_LOAD_CONCURRENCY,
62
+ load,
63
+ );
64
+ const values: Record<string, string> = {};
65
+ for (const variableSet of variableSets) {
66
+ Object.assign(values, variableSet?.values ?? {});
67
+ }
68
+ return values;
69
+ }
70
+
71
+ /** Rig defaults layer below the session's explicitly selected variable set. */
72
+ export function mergeRigDefaultVariableSetEnvironment(
73
+ rigDefaultValues: Record<string, string>,
74
+ sessionValues: Record<string, string>,
75
+ ): Record<string, string> {
76
+ return { ...rigDefaultValues, ...sessionValues };
77
+ }
47
78
 
48
79
  export type RigServices = {
49
80
  db: Database;
@@ -92,12 +123,12 @@ export function rigActorForGrant(grant: AccessGrant): string {
92
123
 
93
124
  export async function requireRigForApi(
94
125
  db: Database,
95
- workspaceId: string,
126
+ grant: AccessGrant,
96
127
  rigId: string,
97
128
  ): Promise<Rig> {
98
- const rig = await getRig(db, workspaceId, rigId);
129
+ const rig = await getRig(db, grant, rigId);
99
130
  if (!rig) {
100
- throw new HTTPException(404, { message: "rig not found" });
131
+ throw new HTTPException(404, { message: "sandbox environment not found" });
101
132
  }
102
133
  return rig;
103
134
  }
@@ -112,7 +143,7 @@ export async function requireRigChangeForApi(
112
143
  // RLS + the workspace clause make a cross-workspace id indistinguishable from
113
144
  // missing; the rigId clause keeps the change addressable only under its rig.
114
145
  if (!change || change.rigId !== rigId) {
115
- throw new HTTPException(404, { message: "rig change not found" });
146
+ throw new HTTPException(404, { message: "sandbox environment change not found" });
116
147
  }
117
148
  return change;
118
149
  }
@@ -120,7 +151,7 @@ export async function requireRigChangeForApi(
120
151
  function trimmedRigName(name: string): string {
121
152
  const trimmed = name.trim();
122
153
  if (!trimmed) {
123
- throw new HTTPException(422, { message: "rig name is required" });
154
+ throw new HTTPException(422, { message: "sandbox environment name is required" });
124
155
  }
125
156
  return trimmed;
126
157
  }
@@ -133,7 +164,9 @@ function assertUniqueCheckNames(checks: ReadonlyArray<{ name: string }> | undefi
133
164
  const seen = new Set<string>();
134
165
  for (const check of checks) {
135
166
  if (seen.has(check.name)) {
136
- throw new HTTPException(422, { message: `duplicate rig check name: ${check.name}` });
167
+ throw new HTTPException(422, {
168
+ message: `duplicate sandbox environment check name: ${check.name}`,
169
+ });
137
170
  }
138
171
  seen.add(check.name);
139
172
  }
@@ -143,47 +176,83 @@ function assertUniqueCheckNames(checks: ReadonlyArray<{ name: string }> | undefi
143
176
  // cross-workspace id indistinguishable from a missing one, so both map to 422.
144
177
  async function assertVariableSetsExist(
145
178
  db: Database,
146
- workspaceId: string,
179
+ access: AccessGrant,
180
+ rigScope: ResourceAuthorityScope,
147
181
  ids: ReadonlyArray<string> | undefined,
148
182
  ): Promise<void> {
149
- if (!ids || ids.length === 0) {
183
+ if (ids === undefined) {
184
+ return;
185
+ }
186
+ requirePermission(access, "variable-sets:attach");
187
+ if (ids.length === 0) {
150
188
  return;
151
189
  }
190
+ requirePermission(access, "variable-sets:use");
152
191
  const unique = [...new Set(ids)];
153
192
  for (const id of unique) {
154
- const variableSet = await getVariableSet(db, workspaceId, id);
193
+ const variableSet = await getVariableSet(db, access, id);
155
194
  if (!variableSet) {
156
- throw new HTTPException(422, { message: `unknown defaultVariableSetId: ${id}` });
195
+ throw new HTTPException(422, {
196
+ message: `unknown defaultVariableSetId: ${id}`,
197
+ });
198
+ }
199
+ if (!variableSetScopeAllowedForRig(rigScope, variableSet.scope)) {
200
+ throw new HTTPException(422, {
201
+ message: `${rigScope}-scoped sandbox environments cannot use ${variableSet.scope}-scoped variable sets`,
202
+ });
157
203
  }
158
204
  }
159
205
  }
160
206
 
207
+ /** A resource may depend only on Variable Sets whose audience is no narrower
208
+ * than its own. User-owned rigs may use anything visible to their owner. */
209
+ export function variableSetScopeAllowedForRig(
210
+ rigScope: ResourceAuthorityScope,
211
+ variableSetScope: ResourceAuthorityScope,
212
+ ): boolean {
213
+ return (
214
+ rigScope === "user" ||
215
+ variableSetScope === "organization" ||
216
+ (rigScope === "workspace" && variableSetScope === "workspace")
217
+ );
218
+ }
219
+
161
220
  export async function createRigForApi(
162
221
  deps: RigServices,
163
222
  grant: AccessGrant,
164
223
  payload: CreateRigRequest,
224
+ options: { allowOrganization?: boolean } = {},
165
225
  ): Promise<Rig> {
166
- const workspaceId = grant.workspaceId;
167
226
  const name = trimmedRigName(payload.name);
168
227
  assertUniqueCheckNames(payload.checks);
169
- await assertVariableSetsExist(deps.db, workspaceId, payload.defaultVariableSetIds);
170
- if ((await countRigs(deps.db, workspaceId)) >= MAX_RIGS_PER_WORKSPACE) {
228
+ await assertVariableSetsExist(
229
+ deps.db,
230
+ grant,
231
+ payload.scope,
232
+ payload.defaultVariableSetIds.length > 0 ? payload.defaultVariableSetIds : undefined,
233
+ );
234
+ if ((await countRigs(deps.db, grant, payload.scope)) >= MAX_RIGS_PER_WORKSPACE) {
171
235
  throw new HTTPException(422, {
172
- message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`,
236
+ message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} sandbox environments`,
173
237
  });
174
238
  }
175
- if (await getRigByName(deps.db, workspaceId, name)) {
176
- throw new HTTPException(409, { message: `rig name is already in use: ${name}` });
239
+ if (await getRigByName(deps.db, grant, name, payload.scope)) {
240
+ throw new HTTPException(409, {
241
+ message: `sandbox environment name is already in use: ${name}`,
242
+ });
177
243
  }
178
244
  const createdBy = rigActorForGrant(grant);
179
245
  const rig = await createRig(deps.db, {
180
246
  accountId: grant.accountId,
181
- workspaceId,
247
+ workspaceId: grant.workspaceId,
248
+ scope: payload.scope,
249
+ subjectId: grant.subjectId,
250
+ allowOrganization: options.allowOrganization === true,
182
251
  name,
183
252
  description: payload.description ?? null,
184
253
  createdBy,
185
254
  initialVersion: {
186
- image: payload.image ?? null,
255
+ image: null,
187
256
  setupScript: payload.setupScript ?? null,
188
257
  checks: payload.checks,
189
258
  credentialHooks: payload.credentialHooks,
@@ -192,7 +261,11 @@ export async function createRigForApi(
192
261
  createdBy,
193
262
  },
194
263
  });
195
- await recordRigAuditEvent(deps.db, { grant, action: "rig.created", rigId: rig.id });
264
+ await recordRigAuditEvent(deps.db, {
265
+ grant,
266
+ action: "rig.created",
267
+ rigId: rig.id,
268
+ });
196
269
  return rig;
197
270
  }
198
271
 
@@ -201,20 +274,27 @@ export async function updateRigForApi(
201
274
  grant: AccessGrant,
202
275
  rig: Rig,
203
276
  payload: UpdateRigRequest,
277
+ options: { allowOrganization?: boolean } = {},
204
278
  ): Promise<Rig> {
205
- const workspaceId = grant.workspaceId;
206
279
  const name = payload.name !== undefined ? trimmedRigName(payload.name) : undefined;
207
280
  if (name !== undefined && name !== rig.name) {
208
- const existing = await getRigByName(deps.db, workspaceId, name);
281
+ const existing = await getRigByName(deps.db, grant, name, rig.scope);
209
282
  if (existing && existing.id !== rig.id) {
210
- throw new HTTPException(409, { message: `rig name is already in use: ${name}` });
283
+ throw new HTTPException(409, {
284
+ message: `sandbox environment name is already in use: ${name}`,
285
+ });
211
286
  }
212
287
  }
213
- const updated = await updateRig(deps.db, workspaceId, rig.id, {
288
+ const updated = await updateScopedRig(deps.db, grant, rig.id, {
214
289
  ...(name !== undefined ? { name } : {}),
215
290
  ...(payload.description !== undefined ? { description: payload.description } : {}),
291
+ allowOrganization: options.allowOrganization === true,
292
+ });
293
+ await recordRigAuditEvent(deps.db, {
294
+ grant,
295
+ action: "rig.updated",
296
+ rigId: rig.id,
216
297
  });
217
- await recordRigAuditEvent(deps.db, { grant, action: "rig.updated", rigId: rig.id });
218
298
  return updated;
219
299
  }
220
300
 
@@ -222,18 +302,27 @@ export async function deleteRigForApi(
222
302
  deps: RigServices,
223
303
  grant: AccessGrant,
224
304
  rig: Rig,
305
+ options: { allowOrganization?: boolean } = {},
225
306
  ): Promise<void> {
226
- const workspaceId = grant.workspaceId;
227
- const deleted = await deleteRigIfNoActiveSessions(deps.db, workspaceId, rig.id);
228
- if (deleted.activeSessionCount > 0) {
229
- throw new HTTPException(409, {
230
- message: `rig is referenced by ${deleted.activeSessionCount} active session(s); it cannot be deleted`,
307
+ try {
308
+ await revokeScopedRig(deps.db, grant, rig.id, {
309
+ allowOrganization: options.allowOrganization === true,
231
310
  });
311
+ } catch (error) {
312
+ let current: unknown = error;
313
+ while (current instanceof Error) {
314
+ if (current.message.includes("active sessions")) {
315
+ throw new HTTPException(409, { message: current.message });
316
+ }
317
+ current = current.cause;
318
+ }
319
+ throw error;
232
320
  }
233
- if (!deleted.deleted) {
234
- throw new HTTPException(404, { message: "rig not found" });
235
- }
236
- await recordRigAuditEvent(deps.db, { grant, action: "rig.deleted", rigId: rig.id });
321
+ await recordRigAuditEvent(deps.db, {
322
+ grant,
323
+ action: "rig.deleted",
324
+ rigId: rig.id,
325
+ });
237
326
  }
238
327
 
239
328
  // Records a proposed change against the rig's CURRENT active version (the base
@@ -247,15 +336,18 @@ export async function proposeRigChangeForApi(
247
336
  request: ProposeRigChangeRequest,
248
337
  options: { proposedBy?: string } = {},
249
338
  ): Promise<RigChange> {
250
- const workspaceId = grant.workspaceId;
339
+ const workspaceId = rig.workspaceId;
251
340
  if (!rig.activeVersion) {
252
- throw new HTTPException(422, { message: "rig has no active version to base a change on" });
341
+ throw new HTTPException(422, {
342
+ message: "sandbox environment has no active version to base a change on",
343
+ });
253
344
  }
254
345
  if (request.kind === "definition_edit") {
255
346
  assertUniqueCheckNames(request.payload.checks);
256
347
  await assertVariableSetsExist(
257
348
  deps.db,
258
- workspaceId,
349
+ grant,
350
+ rig.scope,
259
351
  request.payload.defaultVariableSetIds ?? undefined,
260
352
  );
261
353
  }
@@ -320,7 +412,7 @@ async function promoteChangeWithActiveCas(
320
412
  } catch (error) {
321
413
  if (error instanceof RigActiveVersionChangedError) {
322
414
  throw new HTTPException(409, {
323
- message: `rig moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`,
415
+ message: `sandbox environment moved since this change was verified (base ${error.expectedVersionId}, now ${error.actualVersionId ?? "none"}); re-verify before promoting`,
324
416
  });
325
417
  }
326
418
  if (error instanceof RigChangeTransitionError) {
@@ -342,21 +434,25 @@ export async function promoteSetupAppendChange(
342
434
  });
343
435
  }
344
436
  if (change.status !== "proposed" && change.status !== "verifying") {
345
- throw new HTTPException(409, { message: `rig change is ${change.status}; cannot promote` });
437
+ throw new HTTPException(409, {
438
+ message: `sandbox environment change is ${change.status}; cannot promote`,
439
+ });
346
440
  }
347
441
  if (!change.baseVersionId) {
348
- throw new HTTPException(422, { message: "rig change has no base version" });
442
+ throw new HTTPException(422, { message: "sandbox environment change has no base version" });
349
443
  }
350
- const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
444
+ const base = await getRigVersion(deps.db, rig.workspaceId, rig.id, change.baseVersionId);
351
445
  if (!base) {
352
- throw new HTTPException(404, { message: "base rig version not found" });
446
+ throw new HTTPException(404, { message: "base sandbox environment version not found" });
353
447
  }
354
448
  const payload = change.payload as { command?: unknown; note?: unknown };
355
449
  if (typeof payload.command !== "string" || !payload.command.trim()) {
356
- throw new HTTPException(422, { message: "setup_append change is missing command" });
450
+ throw new HTTPException(422, {
451
+ message: "setup_append change is missing command",
452
+ });
357
453
  }
358
454
  const nextDefinition = {
359
- image: base.image,
455
+ image: null,
360
456
  setupScript: appendRigSetupCommand(base.setupScript, payload.command),
361
457
  checks: base.checks,
362
458
  credentialHooks: base.credentialHooks,
@@ -364,7 +460,7 @@ export async function promoteSetupAppendChange(
364
460
  };
365
461
  const { version, change: updated } = await promoteChangeWithActiveCas(
366
462
  deps,
367
- grant.workspaceId,
463
+ rig.workspaceId,
368
464
  rig.id,
369
465
  change.id,
370
466
  {
@@ -382,13 +478,21 @@ export async function promoteSetupAppendChange(
382
478
  grant,
383
479
  action: "rig.change.merged",
384
480
  rigId: rig.id,
385
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
481
+ metadata: {
482
+ changeId: change.id,
483
+ versionId: version.id,
484
+ version: version.version,
485
+ },
386
486
  });
387
487
  await recordRigAuditEvent(deps.db, {
388
488
  grant,
389
489
  action: "rig.version.promoted",
390
490
  rigId: rig.id,
391
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
491
+ metadata: {
492
+ changeId: change.id,
493
+ versionId: version.id,
494
+ version: version.version,
495
+ },
392
496
  });
393
497
  return { change: updated, version };
394
498
  }
@@ -400,10 +504,14 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
400
504
  change: RigChange,
401
505
  ): Promise<{ change: RigChange; version: RigVersion }> {
402
506
  if (change.kind !== "definition_edit") {
403
- throw new HTTPException(422, { message: "only definition_edit changes use explicit promote" });
507
+ throw new HTTPException(422, {
508
+ message: "only definition_edit changes use explicit promote",
509
+ });
404
510
  }
405
511
  if (change.status !== "proposed") {
406
- throw new HTTPException(409, { message: `rig change is ${change.status}; cannot promote` });
512
+ throw new HTTPException(409, {
513
+ message: `sandbox environment change is ${change.status}; cannot promote`,
514
+ });
407
515
  }
408
516
  if (change.verification?.passed !== true) {
409
517
  throw new HTTPException(422, {
@@ -411,14 +519,13 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
411
519
  });
412
520
  }
413
521
  if (!change.baseVersionId) {
414
- throw new HTTPException(422, { message: "rig change has no base version" });
522
+ throw new HTTPException(422, { message: "sandbox environment change has no base version" });
415
523
  }
416
- const base = await getRigVersion(deps.db, grant.workspaceId, rig.id, change.baseVersionId);
524
+ const base = await getRigVersion(deps.db, rig.workspaceId, rig.id, change.baseVersionId);
417
525
  if (!base) {
418
- throw new HTTPException(404, { message: "base rig version not found" });
526
+ throw new HTTPException(404, { message: "base sandbox environment version not found" });
419
527
  }
420
528
  const payload = change.payload as {
421
- image?: unknown;
422
529
  setupScript?: unknown;
423
530
  checks?: unknown;
424
531
  credentialHooks?: unknown;
@@ -426,7 +533,7 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
426
533
  changelog?: unknown;
427
534
  };
428
535
  const nextDefinition = {
429
- image: payload.image === undefined ? base.image : (payload.image as string | null),
536
+ image: null,
430
537
  setupScript:
431
538
  payload.setupScript === undefined ? base.setupScript : (payload.setupScript as string | null),
432
539
  checks: Array.isArray(payload.checks) ? (payload.checks as RigVersion["checks"]) : base.checks,
@@ -439,7 +546,7 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
439
546
  };
440
547
  const { version, change: updated } = await promoteChangeWithActiveCas(
441
548
  deps,
442
- grant.workspaceId,
549
+ rig.workspaceId,
443
550
  rig.id,
444
551
  change.id,
445
552
  {
@@ -457,13 +564,21 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
457
564
  grant,
458
565
  action: "rig.change.merged",
459
566
  rigId: rig.id,
460
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
567
+ metadata: {
568
+ changeId: change.id,
569
+ versionId: version.id,
570
+ version: version.version,
571
+ },
461
572
  });
462
573
  await recordRigAuditEvent(deps.db, {
463
574
  grant,
464
575
  action: "rig.version.promoted",
465
576
  rigId: rig.id,
466
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
577
+ metadata: {
578
+ changeId: change.id,
579
+ versionId: version.id,
580
+ version: version.version,
581
+ },
467
582
  });
468
583
  return { change: updated, version };
469
584
  }
@@ -475,21 +590,22 @@ export async function createRigVersionForApi(
475
590
  payload: RigDefinitionEditPayload,
476
591
  ): Promise<RigVersion> {
477
592
  if (!rig.activeVersion) {
478
- throw new HTTPException(422, { message: "rig has no active version" });
593
+ throw new HTTPException(422, { message: "sandbox environment has no active version" });
479
594
  }
480
595
  assertUniqueCheckNames(payload.checks);
481
596
  await assertVariableSetsExist(
482
597
  deps.db,
483
- grant.workspaceId,
598
+ grant,
599
+ rig.scope,
484
600
  payload.defaultVariableSetIds ?? undefined,
485
601
  );
486
602
  const base = rig.activeVersion;
487
603
  const version = await createRigVersion(
488
604
  deps.db,
489
- grant.workspaceId,
605
+ rig.workspaceId,
490
606
  rig.id,
491
607
  {
492
- image: payload.image === undefined ? base.image : payload.image,
608
+ image: null,
493
609
  setupScript: payload.setupScript === undefined ? base.setupScript : payload.setupScript,
494
610
  checks: payload.checks ?? base.checks,
495
611
  credentialHooks: payload.credentialHooks ?? base.credentialHooks,
@@ -516,7 +632,7 @@ export async function activateRigVersionForApi(
516
632
  rig: Rig,
517
633
  versionId: string,
518
634
  ): Promise<RigVersion> {
519
- const workspaceId = grant.workspaceId;
635
+ const workspaceId = rig.workspaceId;
520
636
  const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
521
637
  await recordRigAuditEvent(deps.db, {
522
638
  grant,
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  export type RigProviderImageDefinition = Pick<
13
13
  RigVersion,
14
- "image" | "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
14
+ "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
15
15
  >;
16
16
 
17
17
  function sha256(value: string): string {
@@ -42,10 +42,9 @@ export function rigProviderImageContentHash(input: {
42
42
  }): string {
43
43
  return sha256(
44
44
  stableJson({
45
- version: 1,
45
+ version: 2,
46
46
  backend: input.backend,
47
47
  sourceImage: input.sourceImage,
48
- image: input.definition.image,
49
48
  setupScript: input.definition.setupScript,
50
49
  checks: input.definition.checks,
51
50
  credentialHooks: input.definition.credentialHooks,