@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.
@@ -9,6 +9,7 @@ import type {
9
9
  import {
10
10
  createScheduledTask,
11
11
  deleteScheduledTask,
12
+ getRig,
12
13
  getScheduledTask,
13
14
  updateScheduledTask,
14
15
  type Database,
@@ -19,8 +20,8 @@ import { requirePermission } from "../access";
19
20
  import type { SessionWorkflowClient } from "../dependencies";
20
21
  import type { ObjectStorageDependency } from "../dependencies";
21
22
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
22
- import { validateEnvironmentAttachment } from "./environments";
23
- import { assertConfiguredModel } from "./sessions";
23
+ import { validateVariableSetAttachment } from "./environments";
24
+ import { assertConfiguredModel, assertWorkspaceModelPolicyAllows } from "./sessions";
24
25
  import {
25
26
  normalizeResources,
26
27
  validateFileResources,
@@ -43,9 +44,9 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
43
44
  }
44
45
  const agentConfig = (rawPayload as { agentConfig?: unknown }).agentConfig;
45
46
  return Boolean(
46
- agentConfig
47
- && typeof agentConfig === "object"
48
- && Object.prototype.hasOwnProperty.call(agentConfig, "tools"),
47
+ agentConfig &&
48
+ typeof agentConfig === "object" &&
49
+ Object.prototype.hasOwnProperty.call(agentConfig, "tools"),
49
50
  );
50
51
  }
51
52
 
@@ -60,21 +61,31 @@ export async function createValidatedScheduledTask(input: {
60
61
  // capability MCP servers, mirroring session creation.
61
62
  toolsProvided?: boolean;
62
63
  // Set for pack-installation-inherited attachments that were already
63
- // authorized with environments:use when the pack was enabled.
64
- environmentPreauthorized?: boolean;
64
+ // authorized with variable-sets:use when the pack was enabled.
65
+ variableSetPreauthorized?: boolean;
65
66
  }): Promise<ScheduledTask> {
66
- const agentConfig = await validateScheduledTaskAgentConfig({ ...input, workspaceId: input.grant.workspaceId });
67
+ const agentConfig = await validateScheduledTaskAgentConfig({
68
+ ...input,
69
+ workspaceId: input.grant.workspaceId,
70
+ });
67
71
  const id = crypto.randomUUID();
68
72
  validateScheduledTaskSchedule(input.payload.schedule);
69
- if (input.payload.environmentId) {
70
- await validateEnvironmentAttachment(
73
+ if (input.payload.variableSetId) {
74
+ await validateVariableSetAttachment(
71
75
  { settings: input.settings, db: input.db },
72
76
  input.grant,
73
77
  input.grant.workspaceId,
74
- input.payload.environmentId,
75
- { preauthorized: input.environmentPreauthorized ?? false },
78
+ input.payload.variableSetId,
79
+ { preauthorized: input.variableSetPreauthorized ?? false },
76
80
  );
77
81
  }
82
+ // The rig is stored on the task and resolved to its ACTIVE version per fire
83
+ // (at dispatch), so validate only that the id names a rig in the workspace —
84
+ // NOT that it has an active version now (that is a fire-time concern). RLS
85
+ // makes a cross-workspace id indistinguishable from missing → both 422.
86
+ if (input.payload.rigId) {
87
+ await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
88
+ }
78
89
  return await createScheduledTask(input.db, {
79
90
  id,
80
91
  accountId: input.grant.accountId,
@@ -86,11 +97,25 @@ export async function createValidatedScheduledTask(input: {
86
97
  runMode: input.payload.runMode,
87
98
  overlapPolicy: input.payload.overlapPolicy,
88
99
  agentConfig,
89
- environmentId: input.payload.environmentId ?? null,
100
+ variableSetId: input.payload.variableSetId ?? null,
101
+ rigId: input.payload.rigId ?? null,
90
102
  metadata: input.payload.metadata,
91
103
  });
92
104
  }
93
105
 
106
+ // Validate a scheduled task's rig reference: it must name a rig in the
107
+ // workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
108
+ async function requireScheduledTaskRig(
109
+ db: Database,
110
+ workspaceId: string,
111
+ rigId: string,
112
+ ): Promise<void> {
113
+ const rig = await getRig(db, workspaceId, rigId);
114
+ if (!rig) {
115
+ throw new HTTPException(422, { message: `unknown rigId: ${rigId}` });
116
+ }
117
+ }
118
+
94
119
  export async function validatedScheduledTaskUpdate(input: {
95
120
  settings: Settings;
96
121
  db: Database;
@@ -121,39 +146,54 @@ export async function validatedScheduledTaskUpdate(input: {
121
146
  if (input.payload.metadata !== undefined) {
122
147
  update.metadata = input.payload.metadata;
123
148
  }
124
- if (input.payload.environmentId !== undefined) {
125
- const nextEnvironmentId = input.payload.environmentId;
126
- if ((input.existing.environmentId ?? null) !== (nextEnvironmentId ?? null)
127
- && input.existing.runMode === "reusable_session"
128
- && input.existing.reusableSessionId) {
129
- throw new HTTPException(409, { message: "cannot change environment of a task with a live reusable session; recreate the task" });
149
+ if (input.payload.variableSetId !== undefined) {
150
+ const nextVariableSetId = input.payload.variableSetId;
151
+ if (
152
+ (input.existing.variableSetId ?? null) !== (nextVariableSetId ?? null) &&
153
+ input.existing.runMode === "reusable_session" &&
154
+ input.existing.reusableSessionId
155
+ ) {
156
+ throw new HTTPException(409, {
157
+ message:
158
+ "cannot change variableSet of a task with a live reusable session; recreate the task",
159
+ });
130
160
  }
131
- if (nextEnvironmentId === null) {
132
- if (input.existing.environmentId !== null) {
161
+ if (nextVariableSetId === null) {
162
+ if (input.existing.variableSetId !== null) {
133
163
  // Detaching is also an attachment change: it strips the secrets a
134
164
  // task's instructions were designed around.
135
- requirePermission(input.grant, "environments:use");
165
+ requirePermission(input.grant, "variable-sets:use");
136
166
  }
137
- update.environmentId = null;
167
+ update.variableSetId = null;
138
168
  } else {
139
- await validateEnvironmentAttachment(
169
+ await validateVariableSetAttachment(
140
170
  { settings: input.settings, db: input.db },
141
171
  input.grant,
142
172
  input.existing.workspaceId,
143
- nextEnvironmentId,
173
+ nextVariableSetId,
144
174
  );
145
- update.environmentId = nextEnvironmentId;
175
+ update.variableSetId = nextVariableSetId;
146
176
  }
147
177
  }
178
+ if (input.payload.rigId !== undefined) {
179
+ // The rig binds fresh per fire, so changing it on a reusable-session task is
180
+ // harmless for the LIVE session (which keeps its own frozen version) and
181
+ // only affects subsequent new-session fires — no live-session guard needed.
182
+ if (input.payload.rigId !== null) {
183
+ await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
184
+ }
185
+ update.rigId = input.payload.rigId;
186
+ }
148
187
  if (input.payload.agentConfig !== undefined) {
149
188
  // Editing the instructions of a task that injects workspace secrets is
150
189
  // equivalent to attaching those secrets to new instructions, so it
151
- // requires environments:use even though plain task edits do not.
152
- const willHaveEnvironment = input.payload.environmentId !== undefined
153
- ? input.payload.environmentId !== null
154
- : Boolean(input.existing.environmentId);
155
- if (willHaveEnvironment) {
156
- requirePermission(input.grant, "environments:use");
190
+ // requires variable-sets:use even though plain task edits do not.
191
+ const willHaveVariableSet =
192
+ input.payload.variableSetId !== undefined
193
+ ? input.payload.variableSetId !== null
194
+ : Boolean(input.existing.variableSetId);
195
+ if (willHaveVariableSet) {
196
+ requirePermission(input.grant, "variable-sets:use");
157
197
  }
158
198
  update.agentConfig = await validateScheduledTaskAgentConfig({
159
199
  settings: input.settings,
@@ -167,7 +207,11 @@ export async function validatedScheduledTaskUpdate(input: {
167
207
  return update;
168
208
  }
169
209
 
170
- export async function requireScheduledTaskForApi(db: Database, workspaceId: string, taskId: string): Promise<ScheduledTask> {
210
+ export async function requireScheduledTaskForApi(
211
+ db: Database,
212
+ workspaceId: string,
213
+ taskId: string,
214
+ ): Promise<ScheduledTask> {
171
215
  const task = await getScheduledTask(db, workspaceId, taskId);
172
216
  if (!task) {
173
217
  throw new HTTPException(404, { message: "scheduled task not found" });
@@ -175,7 +219,10 @@ export async function requireScheduledTaskForApi(db: Database, workspaceId: stri
175
219
  return task;
176
220
  }
177
221
 
178
- export async function restoreScheduledTask(db: Database, task: ScheduledTask): Promise<ScheduledTask> {
222
+ export async function restoreScheduledTask(
223
+ db: Database,
224
+ task: ScheduledTask,
225
+ ): Promise<ScheduledTask> {
179
226
  return await updateScheduledTask(db, task.workspaceId, task.id, {
180
227
  name: task.name,
181
228
  status: task.status,
@@ -184,7 +231,7 @@ export async function restoreScheduledTask(db: Database, task: ScheduledTask): P
184
231
  overlapPolicy: task.overlapPolicy,
185
232
  agentConfig: task.agentConfig,
186
233
  reusableSessionId: task.reusableSessionId,
187
- environmentId: task.environmentId,
234
+ variableSetId: task.variableSetId,
188
235
  metadata: task.metadata,
189
236
  });
190
237
  }
@@ -197,7 +244,9 @@ export async function syncCreatedScheduledTask(input: {
197
244
  try {
198
245
  await input.workflowClient.syncScheduledTask({ task: input.task });
199
246
  } catch (error) {
200
- await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(() => undefined);
247
+ await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
248
+ () => undefined,
249
+ );
201
250
  throw error;
202
251
  }
203
252
  }
@@ -257,7 +306,11 @@ export function manualScheduledTaskTriggerWorkflowId(taskId: string, triggerToke
257
306
  * charge. Shares the stable trigger token with the workflow id so the charge
258
307
  * and the run dedupe together under retry.
259
308
  */
260
- export function manualScheduledTaskTriggerUsageKey(workspaceId: string, taskId: string, triggerToken: string): string {
309
+ export function manualScheduledTaskTriggerUsageKey(
310
+ workspaceId: string,
311
+ taskId: string,
312
+ triggerToken: string,
313
+ ): string {
261
314
  return `agent_run.created:scheduled-trigger:${workspaceId}:${taskId}:${triggerToken}`;
262
315
  }
263
316
 
@@ -275,17 +328,30 @@ async function validateScheduledTaskAgentConfig(input: {
275
328
  // a model the host does not expose). An omitted model inherits the host
276
329
  // default downstream, which is always configured.
277
330
  assertConfiguredModel(input.settings, input.payload.agentConfig.model);
331
+ // Same policy vetting as the session choke points; an omitted model flows
332
+ // through session creation later, where the effective default is vetted.
333
+ await assertWorkspaceModelPolicyAllows(
334
+ input.db,
335
+ input.settings,
336
+ input.workspaceId,
337
+ input.payload.agentConfig.model,
338
+ );
278
339
  const resources = normalizeResources(input.payload.agentConfig.resources ?? []);
279
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(input.db, input.workspaceId, input.settings);
340
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
341
+ input.db,
342
+ input.workspaceId,
343
+ input.settings,
344
+ );
280
345
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
281
346
  // A task whose creator did not choose tools gets the workspace's enabled
282
347
  // capability MCP servers, exactly like a session created without a tools
283
348
  // key. Scheduled runs are sessions too; "no MCP servers at all" was a trap
284
349
  // every pack/template instantiation path kept falling into (a maintenance
285
350
  // task that cannot reach its workspace's notebook MCP cannot do its job).
286
- const tools = (input.toolsProvided ?? true)
287
- ? requestedTools
288
- : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
351
+ const tools =
352
+ (input.toolsProvided ?? true)
353
+ ? requestedTools
354
+ : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
289
355
  const prompt = input.payload.agentConfig.prompt.trim();
290
356
  if (!prompt) {
291
357
  throw new HTTPException(422, { message: "scheduled task prompt is required" });