@opengeni/core 1.0.1 → 2.2.0-canary.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 (99) hide show
  1. package/dist/access/index.d.ts +37 -0
  2. package/dist/application/new-session-drafts.d.ts +8 -1
  3. package/dist/application/session-commands.d.ts +18 -11
  4. package/dist/application/session-tenancy.d.ts +18 -0
  5. package/dist/application/user-resource-grants.d.ts +20 -0
  6. package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
  7. package/dist/chunk-H7X52RI2.js.map +1 -0
  8. package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
  9. package/dist/chunk-VCZFFEPB.js.map +1 -0
  10. package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
  11. package/dist/chunk-ZYCNLYGH.js.map +1 -0
  12. package/dist/dependencies.d.ts +12 -1
  13. package/dist/domain/automations.d.ts +51 -0
  14. package/dist/domain/company-brain-governed-writes.d.ts +70 -0
  15. package/dist/domain/company-profile-agent-admin.d.ts +24 -0
  16. package/dist/domain/connection-authority.d.ts +97 -0
  17. package/dist/domain/connection-authority.js +169 -0
  18. package/dist/domain/connection-authority.js.map +1 -0
  19. package/dist/domain/editable-artifacts/service.d.ts +1 -2
  20. package/dist/domain/environments.d.ts +1 -1
  21. package/dist/domain/github-repository-bindings.d.ts +117 -0
  22. package/dist/domain/governed-learning-activation.d.ts +16 -0
  23. package/dist/domain/governed-learning-evaluator.d.ts +6 -0
  24. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  25. package/dist/domain/memory-slack-delivery.d.ts +1 -4
  26. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  27. package/dist/domain/packs.d.ts +5 -1
  28. package/dist/domain/personal-connection-delegations.d.ts +34 -2
  29. package/dist/domain/pr-review.d.ts +149 -0
  30. package/dist/domain/remember.d.ts +69 -0
  31. package/dist/domain/resources.d.ts +22 -1
  32. package/dist/domain/scheduled-tasks.d.ts +43 -2
  33. package/dist/domain/session-tool-policy.d.ts +1 -1
  34. package/dist/domain/sessions.d.ts +40 -6
  35. package/dist/editable-artifact-live/application.d.ts +10 -2
  36. package/dist/editable-artifact-live/ports.d.ts +0 -1
  37. package/dist/editable-artifact-live/server.d.ts +0 -1
  38. package/dist/editable-artifact-live/ticket.d.ts +0 -1
  39. package/dist/editable-artifact-live/types.d.ts +5 -2
  40. package/dist/editable-artifact-live.js +2 -2
  41. package/dist/editable-artifacts.js +2 -4
  42. package/dist/index.d.ts +13 -1
  43. package/dist/index.js +4827 -1164
  44. package/dist/index.js.map +1 -1
  45. package/dist/rigs/index.d.ts +22 -5
  46. package/dist/sandbox/fleet.d.ts +34 -6
  47. package/dist/sandbox/routing.d.ts +15 -1
  48. package/dist/sandbox/runtime-settings.d.ts +39 -0
  49. package/package.json +16 -11
  50. package/src/access/index.ts +157 -31
  51. package/src/application/new-session-drafts.ts +24 -2
  52. package/src/application/session-commands.ts +305 -71
  53. package/src/application/session-tenancy.ts +321 -0
  54. package/src/application/user-resource-grants.ts +140 -0
  55. package/src/billing/limits.ts +25 -15
  56. package/src/dependencies.ts +12 -1
  57. package/src/domain/automations.ts +196 -0
  58. package/src/domain/capabilities.ts +7 -1
  59. package/src/domain/company-brain-governed-writes.ts +330 -0
  60. package/src/domain/company-profile-agent-admin.ts +55 -0
  61. package/src/domain/connection-authority.ts +329 -0
  62. package/src/domain/editable-artifacts/agent-application.ts +17 -9
  63. package/src/domain/editable-artifacts/service.ts +12 -1
  64. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  65. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  66. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  67. package/src/domain/environments.ts +16 -3
  68. package/src/domain/github-repository-bindings.ts +295 -0
  69. package/src/domain/governed-learning-activation.ts +44 -0
  70. package/src/domain/governed-learning-evaluator.ts +49 -0
  71. package/src/domain/governed-learning-slack-publication.ts +157 -0
  72. package/src/domain/insights.ts +9 -0
  73. package/src/domain/memory-slack-delivery.ts +0 -30
  74. package/src/domain/organization-membership-lifecycle.ts +9 -0
  75. package/src/domain/packs.ts +106 -3
  76. package/src/domain/personal-connection-delegations.ts +532 -51
  77. package/src/domain/pr-review.ts +671 -0
  78. package/src/domain/remember.ts +549 -0
  79. package/src/domain/resources.ts +131 -4
  80. package/src/domain/scheduled-tasks.ts +497 -76
  81. package/src/domain/session-tool-policy.ts +12 -9
  82. package/src/domain/sessions.ts +613 -166
  83. package/src/editable-artifact-live/application.ts +21 -3
  84. package/src/editable-artifact-live/ports.ts +0 -1
  85. package/src/editable-artifact-live/server.ts +13 -7
  86. package/src/editable-artifact-live/ticket.ts +1 -5
  87. package/src/editable-artifact-live/types.ts +6 -2
  88. package/src/editable-artifact-live/wire.ts +0 -10
  89. package/src/index.ts +13 -1
  90. package/src/rigs/index.ts +165 -48
  91. package/src/sandbox/fleet.ts +376 -30
  92. package/src/sandbox/routing.ts +91 -29
  93. package/src/sandbox/runtime-settings.ts +274 -0
  94. package/src/session-authorization.ts +17 -49
  95. package/dist/chunk-6WKPWE5S.js.map +0 -1
  96. package/dist/chunk-JJU6I5XD.js.map +0 -1
  97. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  98. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  99. package/src/domain/durable-learning-slack-publication.ts +0 -202
@@ -23,9 +23,13 @@ import type { EditableArtifactLiveTicket } from "./types";
23
23
 
24
24
  export type EditableArtifactLiveCompatibilityRequest = Readonly<{
25
25
  artifact: EditableArtifact;
26
- protocolVersion: number;
26
+ modality: EditableArtifactModality;
27
+ liveProtocolVersion: number;
27
28
  kernelVersion: string;
28
29
  modelSchemaVersion: number;
30
+ snapshotVersion: number;
31
+ commandProtocolVersion: number;
32
+ committedTransactionProtocolVersion: number;
29
33
  }>;
30
34
 
31
35
  /** A client/runtime protocol tuple cannot be served by the loaded kernel. */
@@ -71,9 +75,13 @@ export type ReadEditableArtifactApplicationInput = Readonly<{
71
75
 
72
76
  export type MintEditableArtifactApplicationTicketInput = ReadEditableArtifactApplicationInput &
73
77
  Readonly<{
74
- protocolVersion: number;
78
+ modality: EditableArtifactModality;
79
+ liveProtocolVersion: number;
75
80
  kernelVersion: string;
76
81
  modelSchemaVersion: number;
82
+ snapshotVersion: number;
83
+ commandProtocolVersion: number;
84
+ committedTransactionProtocolVersion: number;
77
85
  allowEdit: boolean;
78
86
  }>;
79
87
 
@@ -148,9 +156,19 @@ export class EditableArtifactApplication implements EditableArtifactApplicationP
148
156
  const artifact = await this.readArtifact(input);
149
157
  await this.dependencies.compatibility.assertCompatible({
150
158
  artifact,
151
- protocolVersion: positiveInteger(input.protocolVersion, "protocolVersion"),
159
+ modality: input.modality,
160
+ liveProtocolVersion: positiveInteger(input.liveProtocolVersion, "liveProtocolVersion"),
152
161
  kernelVersion: boundedVersion(input.kernelVersion, "kernelVersion"),
153
162
  modelSchemaVersion: positiveInteger(input.modelSchemaVersion, "modelSchemaVersion"),
163
+ snapshotVersion: positiveInteger(input.snapshotVersion, "snapshotVersion"),
164
+ commandProtocolVersion: positiveInteger(
165
+ input.commandProtocolVersion,
166
+ "commandProtocolVersion",
167
+ ),
168
+ committedTransactionProtocolVersion: positiveInteger(
169
+ input.committedTransactionProtocolVersion,
170
+ "committedTransactionProtocolVersion",
171
+ ),
154
172
  });
155
173
  return await this.dependencies.live.mintTicket({
156
174
  scope: artifact.scope,
@@ -40,7 +40,6 @@ export type EditableArtifactLiveBootstrapRead = Readonly<{
40
40
  scope: EditableArtifactScope;
41
41
  artifactId: EditableArtifactId;
42
42
  resume: EditableArtifactLiveResume;
43
- protocolVersion: number;
44
43
  }>;
45
44
 
46
45
  export type EditableArtifactLiveTransactionPage = Readonly<{
@@ -81,7 +81,6 @@ const MAX_IDENTIFIER_BYTES = 512;
81
81
  const MAX_CAUSAL_ACTORS = 1_024;
82
82
 
83
83
  export type EditableArtifactLiveServerOptions = Readonly<{
84
- protocolVersion?: number;
85
84
  ticketTtlMs?: number;
86
85
  maxClientFrameBytes?: number;
87
86
  maxOutboundFrameBytes?: number;
@@ -101,7 +100,10 @@ export type EditableArtifactLiveServerOptions = Readonly<{
101
100
  }>;
102
101
 
103
102
  type NormalizedOptions = Required<Omit<EditableArtifactLiveServerOptions, "ticketTtlMs">> &
104
- Readonly<{ ticketTtlMs?: number }>;
103
+ Readonly<{
104
+ protocolVersion: typeof EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
105
+ ticketTtlMs?: number;
106
+ }>;
105
107
 
106
108
  export type OpenEditableArtifactLiveInput = Readonly<{
107
109
  token: string;
@@ -147,7 +149,6 @@ export class EditableArtifactLiveServer {
147
149
  tickets: dependencies.tickets,
148
150
  tokens: dependencies.tokens,
149
151
  clock: dependencies.clock,
150
- protocolVersion: this.options.protocolVersion,
151
152
  ...(this.options.ticketTtlMs === undefined ? {} : { ttlMs: this.options.ticketTtlMs }),
152
153
  });
153
154
  }
@@ -312,7 +313,6 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
312
313
  scope: this.ticket.scope,
313
314
  artifactId: this.artifactId,
314
315
  resume: this.resume,
315
- protocolVersion: this.options.protocolVersion,
316
316
  });
317
317
  this.assertOpen();
318
318
  validateBootstrap(bootstrap, this.artifactId, this.modality, this.resume, this.options);
@@ -706,7 +706,13 @@ class EditableArtifactLiveSessionImpl implements EditableArtifactLiveSession {
706
706
  } as const;
707
707
  await this.send(
708
708
  snapshot.modality === "spreadsheet"
709
- ? { ...common, modality: snapshot.modality, causalFrontier: causalFrontier! }
709
+ ? {
710
+ ...common,
711
+ modality: snapshot.modality,
712
+ causalFrontier: causalFrontier!,
713
+ operationProtocolVersion: snapshot.operationProtocolVersion,
714
+ snapshotVersion: snapshot.snapshotVersion,
715
+ }
710
716
  : {
711
717
  ...common,
712
718
  modality: snapshot.modality,
@@ -1264,7 +1270,7 @@ function validateResume(
1264
1270
  resume: EditableArtifactLiveResume,
1265
1271
  durableModality: EditableArtifactModality,
1266
1272
  ): void {
1267
- const resumeModality = resume.modality ?? "spreadsheet";
1273
+ const resumeModality = resume.modality;
1268
1274
  if (resumeModality !== durableModality) {
1269
1275
  throw new EditableArtifactLiveError(
1270
1276
  "invalid_frame",
@@ -1608,7 +1614,7 @@ function boundedString(value: string, label: string, maxBytes: number): void {
1608
1614
 
1609
1615
  function normalizeOptions(options: EditableArtifactLiveServerOptions): NormalizedOptions {
1610
1616
  const normalized = {
1611
- protocolVersion: options.protocolVersion ?? EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
1617
+ protocolVersion: EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION,
1612
1618
  maxClientFrameBytes: options.maxClientFrameBytes ?? DEFAULT_MAX_CLIENT_FRAME_BYTES,
1613
1619
  maxOutboundFrameBytes: options.maxOutboundFrameBytes ?? DEFAULT_MAX_OUTBOUND_FRAME_BYTES,
1614
1620
  maxSnapshotBytes: options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES,
@@ -31,7 +31,6 @@ export type EditableArtifactLiveTicketAuthorityDependencies = Readonly<{
31
31
  tokens: EditableArtifactLiveTokenPort;
32
32
  clock: EditableArtifactLiveClockPort;
33
33
  ttlMs?: number;
34
- protocolVersion?: number;
35
34
  }>;
36
35
 
37
36
  export class EditableArtifactLiveTicketAuthority {
@@ -43,10 +42,7 @@ export class EditableArtifactLiveTicketAuthority {
43
42
  if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs < 1_000 || this.ttlMs > MAX_TTL_MS) {
44
43
  throw new TypeError(`ticket ttl must be 1000-${MAX_TTL_MS} milliseconds`);
45
44
  }
46
- this.protocolVersion = dependencies.protocolVersion ?? EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
47
- if (!Number.isSafeInteger(this.protocolVersion) || this.protocolVersion < 1) {
48
- throw new TypeError("protocol version must be a positive safe integer");
49
- }
45
+ this.protocolVersion = EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION;
50
46
  }
51
47
 
52
48
  async mint(
@@ -1,3 +1,4 @@
1
+ import { EDITABLE_ARTIFACT_LIVE_WIRE_VERSION } from "@opengeni/contracts/editable-artifact-live";
1
2
  import type {
2
3
  EditableArtifactActor,
3
4
  EditableArtifactCausalFrontier,
@@ -11,7 +12,7 @@ import type {
11
12
  EditableArtifactTransactionId,
12
13
  } from "../domain/editable-artifacts/types";
13
14
 
14
- export const EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION = 1;
15
+ export const EDITABLE_ARTIFACT_LIVE_PROTOCOL_VERSION = EDITABLE_ARTIFACT_LIVE_WIRE_VERSION;
15
16
 
16
17
  export type EditableArtifactLiveTicket = Readonly<{
17
18
  artifactId: EditableArtifactId;
@@ -52,6 +53,7 @@ export type EditableArtifactLiveSnapshot =
52
53
  modality: "spreadsheet";
53
54
  causalFrontier: EditableArtifactCausalFrontier;
54
55
  operationProtocolVersion: number;
56
+ snapshotVersion: number;
55
57
  }>)
56
58
  | (EditableArtifactLiveSnapshotCommon &
57
59
  Readonly<{
@@ -120,7 +122,7 @@ type EditableArtifactLiveResumeCommon = Readonly<{
120
122
  export type EditableArtifactLiveResume =
121
123
  | (EditableArtifactLiveResumeCommon &
122
124
  Readonly<{
123
- modality?: "spreadsheet";
125
+ modality: "spreadsheet";
124
126
  localCausalFrontier: EditableArtifactCausalFrontier;
125
127
  }>)
126
128
  | (EditableArtifactLiveResumeCommon &
@@ -166,6 +168,8 @@ export type EditableArtifactLiveServerFrame =
166
168
  | Readonly<{
167
169
  modality: "spreadsheet";
168
170
  causalFrontier: EditableArtifactCausalFrontier;
171
+ operationProtocolVersion: number;
172
+ snapshotVersion: number;
169
173
  }>
170
174
  | Readonly<{
171
175
  modality: "document" | "presentation";
@@ -81,15 +81,5 @@ export function encodeEditableArtifactLiveMutationWireFrame(
81
81
  export function encodeEditableArtifactLiveServerWireFrame(
82
82
  frame: EditableArtifactLiveServerFrame,
83
83
  ): Uint8Array {
84
- if (frame.type === "transaction" && frame.transaction.modality === "spreadsheet") {
85
- const { operationProtocolVersion, modality: _modality, ...transaction } = frame.transaction;
86
- return encodeContractServerFrame({
87
- ...frame,
88
- transaction: {
89
- ...transaction,
90
- protocolVersion: operationProtocolVersion,
91
- },
92
- } as unknown as ContractServerFrame);
93
- }
94
84
  return encodeContractServerFrame(frame as unknown as ContractServerFrame);
95
85
  }
package/src/index.ts CHANGED
@@ -43,6 +43,7 @@ export * from "./transcription";
43
43
  // fleet tools, the machines REST route, and the rest of the sandbox layer.
44
44
  export * from "./sandbox/fleet";
45
45
  export * from "./sandbox/routing";
46
+ export * from "./sandbox/runtime-settings";
46
47
 
47
48
  // Access layer (transport-neutral grant resolution + permission checks).
48
49
  export * from "./access";
@@ -59,25 +60,36 @@ export * from "./domain/skill-imports";
59
60
  export * from "./domain/environments";
60
61
  export * from "./rigs";
61
62
  export * from "./domain/packs";
63
+ export * from "./domain/automations";
64
+ export * from "./domain/pr-review";
62
65
  export * from "./domain/personal-connection-delegations";
63
66
  export * from "./domain/resources";
67
+ export * from "./domain/github-repository-bindings";
64
68
  export * from "./domain/session-tool-policy";
65
69
  export * from "./domain/scheduled-tasks";
66
70
  export * from "./domain/sessions";
67
71
  export * from "./domain/insights";
68
72
  export * from "./domain/memory-slack-publication";
69
73
  export * from "./domain/memory-slack-delivery";
70
- export * from "./domain/durable-learning-slack-publication";
74
+ export * from "./domain/governed-learning-slack-publication";
71
75
  export * from "./domain/slack-publication-secret-safety";
72
76
  export * from "./domain/company-profile-durable-learning-adapter";
77
+ export * from "./domain/company-profile-agent-admin";
78
+ export * from "./domain/company-brain-governed-writes";
79
+ export * from "./domain/governed-learning-evaluator";
80
+ export * from "./domain/governed-learning-activation";
81
+ export * from "./domain/remember";
73
82
  export * from "./domain/slack-bot";
74
83
  export * from "./domain/conversation-integrations";
75
84
  export * from "./domain/fiken";
76
85
  export * from "./domain/workspace-members";
77
86
  export * from "./domain/video-generation";
78
87
  export * from "./domain/video-generation-capabilities";
88
+ export * from "./domain/organization-membership-lifecycle";
79
89
  export * from "./application/new-session-drafts";
80
90
  export * from "./application/session-commands";
91
+ export * from "./application/session-tenancy";
92
+ export * from "./application/user-resource-grants";
81
93
 
82
94
  // Durable editable-artifact live broker, ticket, ports, and projection types.
83
95
  export * from "./editable-artifact-live";
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,10 +123,10 @@ 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
131
  throw new HTTPException(404, { message: "rig not found" });
101
132
  }
@@ -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 rig check name: ${check.name}`,
169
+ });
137
170
  }
138
171
  seen.add(check.name);
139
172
  }
@@ -143,42 +176,78 @@ 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 rigs 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
236
  message: `a workspace supports at most ${MAX_RIGS_PER_WORKSPACE} rigs`,
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: `rig 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,
@@ -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: `rig 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: "rig 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
  }
@@ -342,18 +434,22 @@ 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: `rig change is ${change.status}; cannot promote`,
439
+ });
346
440
  }
347
441
  if (!change.baseVersionId) {
348
442
  throw new HTTPException(422, { message: "rig 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
446
  throw new HTTPException(404, { message: "base rig 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
455
  image: base.image,
@@ -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: `rig change is ${change.status}; cannot promote`,
514
+ });
407
515
  }
408
516
  if (change.verification?.passed !== true) {
409
517
  throw new HTTPException(422, {
@@ -413,7 +521,7 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
413
521
  if (!change.baseVersionId) {
414
522
  throw new HTTPException(422, { message: "rig 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
526
  throw new HTTPException(404, { message: "base rig version not found" });
419
527
  }
@@ -439,7 +547,7 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
439
547
  };
440
548
  const { version, change: updated } = await promoteChangeWithActiveCas(
441
549
  deps,
442
- grant.workspaceId,
550
+ rig.workspaceId,
443
551
  rig.id,
444
552
  change.id,
445
553
  {
@@ -457,13 +565,21 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
457
565
  grant,
458
566
  action: "rig.change.merged",
459
567
  rigId: rig.id,
460
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
568
+ metadata: {
569
+ changeId: change.id,
570
+ versionId: version.id,
571
+ version: version.version,
572
+ },
461
573
  });
462
574
  await recordRigAuditEvent(deps.db, {
463
575
  grant,
464
576
  action: "rig.version.promoted",
465
577
  rigId: rig.id,
466
- metadata: { changeId: change.id, versionId: version.id, version: version.version },
578
+ metadata: {
579
+ changeId: change.id,
580
+ versionId: version.id,
581
+ version: version.version,
582
+ },
467
583
  });
468
584
  return { change: updated, version };
469
585
  }
@@ -480,13 +596,14 @@ export async function createRigVersionForApi(
480
596
  assertUniqueCheckNames(payload.checks);
481
597
  await assertVariableSetsExist(
482
598
  deps.db,
483
- grant.workspaceId,
599
+ grant,
600
+ rig.scope,
484
601
  payload.defaultVariableSetIds ?? undefined,
485
602
  );
486
603
  const base = rig.activeVersion;
487
604
  const version = await createRigVersion(
488
605
  deps.db,
489
- grant.workspaceId,
606
+ rig.workspaceId,
490
607
  rig.id,
491
608
  {
492
609
  image: payload.image === undefined ? base.image : payload.image,
@@ -516,7 +633,7 @@ export async function activateRigVersionForApi(
516
633
  rig: Rig,
517
634
  versionId: string,
518
635
  ): Promise<RigVersion> {
519
- const workspaceId = grant.workspaceId;
636
+ const workspaceId = rig.workspaceId;
520
637
  const version = await activateRigVersion(deps.db, workspaceId, rig.id, versionId);
521
638
  await recordRigAuditEvent(deps.db, {
522
639
  grant,