@opengeni/core 0.21.2 → 0.24.1

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 (123) hide show
  1. package/dist/access/index.d.ts +9 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +9 -4
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +35 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/memory-slack-publication.d.ts +0 -2
  32. package/dist/domain/packs.d.ts +29 -1
  33. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  34. package/dist/domain/scheduled-tasks.d.ts +27 -3
  35. package/dist/domain/session-tool-policy.d.ts +2 -2
  36. package/dist/domain/sessions.d.ts +44 -10
  37. package/dist/domain/skill-imports.d.ts +32 -0
  38. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  39. package/dist/domain/timeline-annotations.d.ts +5 -0
  40. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  41. package/dist/domain/video-generation.d.ts +47 -0
  42. package/dist/editable-artifact-live/application.d.ts +69 -0
  43. package/dist/editable-artifact-live/errors.d.ts +1 -0
  44. package/dist/editable-artifact-live/index.d.ts +8 -0
  45. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  46. package/dist/editable-artifact-live/ports.d.ts +100 -0
  47. package/dist/editable-artifact-live/server.d.ts +71 -0
  48. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  49. package/dist/editable-artifact-live/types.d.ts +213 -0
  50. package/dist/editable-artifact-live/wire.d.ts +34 -0
  51. package/dist/editable-artifact-live.d.ts +2 -0
  52. package/dist/editable-artifact-live.js +58 -0
  53. package/dist/editable-artifact-live.js.map +1 -0
  54. package/dist/editable-artifacts.d.ts +2 -0
  55. package/dist/editable-artifacts.js +179 -0
  56. package/dist/editable-artifacts.js.map +1 -0
  57. package/dist/index.d.ts +35 -25
  58. package/dist/index.js +3184 -652
  59. package/dist/index.js.map +1 -1
  60. package/dist/managed-session.d.ts +53 -0
  61. package/dist/rigs/index.d.ts +1 -1
  62. package/dist/rigs/provider-images.d.ts +31 -0
  63. package/dist/sandbox/fleet.d.ts +9 -1
  64. package/dist/sandbox/routing.d.ts +1 -0
  65. package/dist/session-authorization.d.ts +10 -1
  66. package/dist/workflow-wake-contract.d.ts +7 -0
  67. package/package.json +18 -10
  68. package/src/access/index.ts +43 -3
  69. package/src/application/new-session-drafts.ts +2 -0
  70. package/src/application/session-commands.ts +217 -116
  71. package/src/dependencies.ts +30 -2
  72. package/src/domain/capabilities.ts +672 -218
  73. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  74. package/src/domain/durable-learning-slack-publication.ts +202 -0
  75. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  76. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  77. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  78. package/src/domain/editable-artifacts/errors.ts +115 -0
  79. package/src/domain/editable-artifacts/hash.ts +131 -0
  80. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  81. package/src/domain/editable-artifacts/index.ts +14 -0
  82. package/src/domain/editable-artifacts/ports.ts +462 -0
  83. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  84. package/src/domain/editable-artifacts/service.ts +2402 -0
  85. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  86. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  87. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  88. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  89. package/src/domain/editable-artifacts/types.ts +835 -0
  90. package/src/domain/insights.ts +136 -33
  91. package/src/domain/memory-slack-delivery.ts +204 -0
  92. package/src/domain/memory-slack-publication.ts +10 -19
  93. package/src/domain/packs.ts +266 -5
  94. package/src/domain/personal-connection-delegations.ts +97 -6
  95. package/src/domain/resources.ts +10 -20
  96. package/src/domain/scheduled-tasks.ts +365 -25
  97. package/src/domain/session-tool-policy.ts +8 -2
  98. package/src/domain/sessions.ts +388 -155
  99. package/src/domain/skill-imports.ts +350 -0
  100. package/src/domain/slack-publication-secret-safety.ts +42 -0
  101. package/src/domain/timeline-annotations.ts +204 -0
  102. package/src/domain/video-generation-capabilities.ts +69 -0
  103. package/src/domain/video-generation.ts +249 -0
  104. package/src/editable-artifact-live/application.ts +182 -0
  105. package/src/editable-artifact-live/errors.ts +4 -0
  106. package/src/editable-artifact-live/index.ts +8 -0
  107. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  108. package/src/editable-artifact-live/ports.ts +139 -0
  109. package/src/editable-artifact-live/server.ts +1707 -0
  110. package/src/editable-artifact-live/ticket.ts +217 -0
  111. package/src/editable-artifact-live/types.ts +286 -0
  112. package/src/editable-artifact-live/wire.ts +95 -0
  113. package/src/editable-artifact-live.ts +2 -0
  114. package/src/editable-artifacts.ts +2 -0
  115. package/src/index.ts +14 -0
  116. package/src/managed-auth-type.ts +4 -3
  117. package/src/managed-session.ts +36 -0
  118. package/src/rigs/index.ts +26 -19
  119. package/src/rigs/provider-images.ts +110 -0
  120. package/src/sandbox/fleet.ts +56 -1
  121. package/src/sandbox/routing.ts +32 -3
  122. package/src/session-authorization.ts +20 -0
  123. package/src/workflow-wake-contract.ts +8 -0
@@ -2,8 +2,12 @@ import type { Settings } from "@opengeni/config";
2
2
  import type {
3
3
  AccessGrant,
4
4
  McpPersonalConnectionDelegation,
5
+ KnowledgeSourceSyncAction,
5
6
  ScheduledTask,
6
7
  ScheduledTaskAgentConfig,
8
+ Session,
9
+ SessionAuthorizationPort,
10
+ SessionAuthorizationSurface,
7
11
  CreateScheduledTaskRequest as CreateScheduledTaskPayload,
8
12
  UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
9
13
  } from "@opengeni/contracts";
@@ -11,17 +15,26 @@ import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY } from "@opengeni/contracts";
11
15
  import {
12
16
  createScheduledTask,
13
17
  deleteScheduledTask,
18
+ getConnectionMetadata,
19
+ getKnowledgeSourceForSyncAuthority,
14
20
  getNestedAgentDepthDeploymentPolicy,
15
21
  getRig,
16
22
  getScheduledTask,
17
23
  getScheduledTaskPersonalConnectionDelegations,
24
+ getSession,
18
25
  requireWorkspace,
26
+ scopedKnowledgeScopeKey,
19
27
  updateScheduledTask,
20
28
  type Database,
21
29
  type UpdateScheduledTaskInput,
22
30
  } from "@opengeni/db";
23
31
  import { HTTPException } from "hono/http-exception";
24
32
  import { hasPermission, requirePermission } from "../access";
33
+ import {
34
+ requireSessionAuthorization,
35
+ SessionAuthorizationDeniedError,
36
+ SessionAuthorizationUnavailableError,
37
+ } from "../session-authorization";
25
38
  import type { SessionWorkflowClient } from "../dependencies";
26
39
  import type { ObjectStorageDependency } from "../dependencies";
27
40
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
@@ -38,6 +51,7 @@ import {
38
51
  } from "./sessions";
39
52
  import {
40
53
  hasReservedOpenGeniSlackBotSessionMetadata,
54
+ scheduledSlackBotConnectionId,
41
55
  validateOpenGeniSlackBotConnectionSelection,
42
56
  } from "./slack-bot";
43
57
  import {
@@ -81,14 +95,42 @@ export async function createValidatedScheduledTask(input: {
81
95
  // Set for pack-installation-inherited attachments that were already
82
96
  // authorized with variable-sets:use when the pack was enabled.
83
97
  variableSetPreauthorized?: boolean;
98
+ sessionAuthorization?: SessionAuthorizationPort | null | undefined;
99
+ authorizationSurface?: SessionAuthorizationSurface | undefined;
84
100
  }): Promise<ScheduledTask> {
85
- const agentConfig = await validateScheduledTaskAgentConfig({
86
- ...input,
87
- workspaceId: input.grant.workspaceId,
88
- });
101
+ // API parsing fills this default, but pack installers and older internal
102
+ // callers can still invoke the shared validator with the pre-action shape.
103
+ const action = input.payload.action ?? ({ kind: "agent_turn" } as const);
104
+ const knowledgeAction = action.kind === "knowledge_source_sync" ? action : null;
105
+ if (knowledgeAction) {
106
+ await validateKnowledgeSourceSyncAction({
107
+ db: input.db,
108
+ grant: input.grant,
109
+ action: knowledgeAction,
110
+ });
111
+ }
112
+ const agentConfig = knowledgeAction
113
+ ? input.payload.agentConfig
114
+ : await validateScheduledTaskAgentConfig({
115
+ ...input,
116
+ workspaceId: input.grant.workspaceId,
117
+ });
89
118
  const id = crypto.randomUUID();
90
119
  validateScheduledTaskSchedule(input.payload.schedule);
91
- if (input.payload.variableSetId) {
120
+ const target = knowledgeAction
121
+ ? null
122
+ : await validateScheduledTaskTarget({
123
+ db: input.db,
124
+ sessionAuthorization: input.sessionAuthorization,
125
+ authorizationSurface: input.authorizationSurface,
126
+ grant: input.grant,
127
+ targetSessionId: input.payload.targetSessionId,
128
+ runMode: input.payload.runMode,
129
+ variableSetId: input.payload.variableSetId,
130
+ rigId: input.payload.rigId,
131
+ agentConfig,
132
+ });
133
+ if (!knowledgeAction && input.payload.variableSetId) {
92
134
  await validateVariableSetAttachment(
93
135
  { settings: input.settings, db: input.db },
94
136
  input.grant,
@@ -101,21 +143,23 @@ export async function createValidatedScheduledTask(input: {
101
143
  // (at dispatch), so validate only that the id names a rig in the workspace —
102
144
  // NOT that it has an active version now (that is a fire-time concern). RLS
103
145
  // makes a cross-workspace id indistinguishable from missing → both 422.
104
- if (input.payload.rigId) {
146
+ if (!knowledgeAction && input.payload.rigId) {
105
147
  await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
106
148
  }
107
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
108
- input.db,
109
- input.grant.workspaceId,
110
- input.settings,
111
- );
112
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
113
- db: input.db,
114
- workspaceId: input.grant.workspaceId,
115
- settings: runtimeSettings,
116
- tools: [...agentConfig.tools, { kind: "mcp", id: "opengeni" }],
117
- source: personalConnectionDelegationSourceForGrant(input.grant),
118
- });
149
+ const personalConnectionDelegations = knowledgeAction
150
+ ? []
151
+ : await freezePersonalConnectionDelegations({
152
+ db: input.db,
153
+ workspaceId: input.grant.workspaceId,
154
+ settings: await settingsWithEnabledCapabilityMcpServers(
155
+ input.db,
156
+ input.grant.workspaceId,
157
+ input.settings,
158
+ { subjectId: input.grant.subjectId },
159
+ ),
160
+ tools: [...agentConfig.tools, { kind: "mcp", id: "opengeni" }],
161
+ source: personalConnectionDelegationSourceForGrant(input.grant),
162
+ });
119
163
  const creationInitiator = creationInitiatorForGrant(input.grant);
120
164
  return await createScheduledTask(input.db, {
121
165
  id,
@@ -127,17 +171,132 @@ export async function createValidatedScheduledTask(input: {
127
171
  temporalScheduleId: scheduledTaskTemporalScheduleId(id),
128
172
  runMode: input.payload.runMode,
129
173
  overlapPolicy: input.payload.overlapPolicy,
174
+ action,
130
175
  agentConfig,
131
176
  ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
132
177
  ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
133
178
  createdByActor: creationInitiator.actor ?? null,
134
179
  personalConnectionDelegations,
180
+ targetSessionId: target?.id ?? null,
135
181
  variableSetId: input.payload.variableSetId ?? null,
136
182
  rigId: input.payload.rigId ?? null,
137
183
  metadata: input.payload.metadata,
138
184
  });
139
185
  }
140
186
 
187
+ export async function validateScheduledTaskTarget(input: {
188
+ db: Database;
189
+ sessionAuthorization?: SessionAuthorizationPort | null | undefined;
190
+ authorizationSurface?: SessionAuthorizationSurface | undefined;
191
+ grant: AccessGrant;
192
+ targetSessionId: string | null | undefined;
193
+ runMode: ScheduledTask["runMode"];
194
+ variableSetId: string | null | undefined;
195
+ rigId: string | null | undefined;
196
+ agentConfig: ScheduledTaskAgentConfig;
197
+ missingTargetStatus?: 404 | 422;
198
+ }): Promise<Session | null> {
199
+ if (input.runMode !== "existing_session") {
200
+ if (input.targetSessionId) {
201
+ throw new HTTPException(422, {
202
+ message: "targetSessionId requires runMode=existing_session",
203
+ });
204
+ }
205
+ return null;
206
+ }
207
+ if (!input.targetSessionId) {
208
+ throw new HTTPException(input.missingTargetStatus ?? 422, {
209
+ message:
210
+ input.missingTargetStatus === 404
211
+ ? "target session not found"
212
+ : "targetSessionId is required when runMode=existing_session",
213
+ });
214
+ }
215
+ requirePermission(input.grant, "sessions:control");
216
+ if (input.agentConfig.goal) {
217
+ throw new HTTPException(422, {
218
+ message: "agentConfig.goal cannot be used with an existing-session target",
219
+ });
220
+ }
221
+ try {
222
+ await requireSessionAuthorization(
223
+ {
224
+ db: input.db,
225
+ ...(input.sessionAuthorization !== undefined
226
+ ? { sessionAuthorization: input.sessionAuthorization }
227
+ : {}),
228
+ },
229
+ input.grant,
230
+ {
231
+ sessionId: input.targetSessionId,
232
+ operation: "session.control",
233
+ surface: input.authorizationSurface ?? "http",
234
+ },
235
+ );
236
+ } catch (error) {
237
+ if (error instanceof SessionAuthorizationDeniedError) {
238
+ throw new HTTPException(404, { message: "target session not found" });
239
+ }
240
+ if (error instanceof SessionAuthorizationUnavailableError) {
241
+ throw new HTTPException(503, { message: "session authorization is unavailable" });
242
+ }
243
+ throw error;
244
+ }
245
+ const session = await getSession(input.db, input.grant.workspaceId, input.targetSessionId);
246
+ if (!session || session.accountId !== input.grant.accountId) {
247
+ throw new HTTPException(404, { message: "target session not found" });
248
+ }
249
+ if (session.status === "cancelled") {
250
+ throw new HTTPException(409, {
251
+ message: "target session is cancelled; choose a revivable session",
252
+ });
253
+ }
254
+ if ((session.variableSetId ?? null) !== (input.variableSetId ?? null)) {
255
+ throw new HTTPException(422, {
256
+ message: "target session variableSet attachment does not match the scheduled task",
257
+ });
258
+ }
259
+ if (input.rigId && input.rigId !== session.rigId) {
260
+ throw new HTTPException(422, {
261
+ message: "target session rig does not match the scheduled task",
262
+ });
263
+ }
264
+ if (
265
+ input.agentConfig.sandboxBackend !== undefined &&
266
+ input.agentConfig.sandboxBackend !== session.sandboxBackend
267
+ ) {
268
+ throw new HTTPException(422, {
269
+ message: "target session sandbox backend does not match the scheduled task",
270
+ });
271
+ }
272
+ if (
273
+ scheduledSlackBotConnectionId(session.metadata) !==
274
+ (input.agentConfig.slackBotConnectionId ?? null)
275
+ ) {
276
+ throw new HTTPException(422, {
277
+ message: "target session OpenGeni Slack bot binding does not match the scheduled task",
278
+ });
279
+ }
280
+ return session;
281
+ }
282
+
283
+ export function scheduledTaskForGrant(task: ScheduledTask, grant: AccessGrant): ScheduledTask {
284
+ if (hasPermission(grant.permissions, "sessions:control") || task.targetSessionId === null) {
285
+ return task;
286
+ }
287
+ return { ...task, targetSessionId: null };
288
+ }
289
+
290
+ export function scheduledTaskRunForGrant<T extends { sessionId: string | null }>(
291
+ run: T,
292
+ grant: AccessGrant,
293
+ ): T {
294
+ if (hasPermission(grant.permissions, "sessions:control") || run.sessionId === null) {
295
+ return run;
296
+ }
297
+ return { ...run, sessionId: null };
298
+ }
299
+
141
300
  // Validate a scheduled task's rig reference: it must name a rig in the
142
301
  // workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
143
302
  async function requireScheduledTaskRig(
@@ -160,8 +319,72 @@ export async function validatedScheduledTaskUpdate(input: {
160
319
  payload: UpdateScheduledTaskPayload;
161
320
  /** See createValidatedScheduledTask; only consulted when agentConfig is updated. */
162
321
  toolsProvided?: boolean;
322
+ sessionAuthorization?: SessionAuthorizationPort | null | undefined;
323
+ authorizationSurface?: SessionAuthorizationSurface | undefined;
163
324
  }): Promise<UpdateScheduledTaskInput> {
164
325
  const update: UpdateScheduledTaskInput = {};
326
+ const existingKnowledge = input.existing.action.kind === "knowledge_source_sync";
327
+ if (input.payload.action && input.payload.action.kind !== input.existing.action.kind) {
328
+ throw new HTTPException(409, {
329
+ message: "scheduled task action kind is immutable; create a new schedule",
330
+ });
331
+ }
332
+ if (existingKnowledge) {
333
+ if (
334
+ input.payload.agentConfig !== undefined ||
335
+ input.payload.runMode !== undefined ||
336
+ input.payload.targetSessionId !== undefined ||
337
+ input.payload.variableSetId !== undefined ||
338
+ input.payload.rigId !== undefined
339
+ ) {
340
+ throw new HTTPException(422, {
341
+ message: "knowledge source schedules do not accept agent/session configuration",
342
+ });
343
+ }
344
+ if (input.payload.overlapPolicy === "allow_concurrent") {
345
+ throw new HTTPException(422, {
346
+ message: "knowledge source schedules require skip or buffer_one overlap",
347
+ });
348
+ }
349
+ if (input.payload.action?.kind === "knowledge_source_sync") {
350
+ await validateKnowledgeSourceSyncAction({
351
+ db: input.db,
352
+ grant: input.grant,
353
+ action: input.payload.action,
354
+ });
355
+ update.action = input.payload.action;
356
+ }
357
+ if (input.payload.name !== undefined)
358
+ update.name = trimmedScheduledTaskName(input.payload.name);
359
+ if (input.payload.status !== undefined) update.status = input.payload.status;
360
+ if (input.payload.schedule !== undefined) {
361
+ validateScheduledTaskSchedule(input.payload.schedule);
362
+ update.schedule = input.payload.schedule;
363
+ }
364
+ if (input.payload.overlapPolicy !== undefined) {
365
+ update.overlapPolicy = input.payload.overlapPolicy;
366
+ }
367
+ if (input.payload.metadata !== undefined) update.metadata = input.payload.metadata;
368
+ return update;
369
+ }
370
+ const existingTarget = input.existing.targetSessionId;
371
+ const nextRunMode = input.payload.runMode ?? input.existing.runMode;
372
+ const nextTargetSessionId =
373
+ input.payload.targetSessionId !== undefined
374
+ ? input.payload.targetSessionId
375
+ : nextRunMode === "existing_session"
376
+ ? existingTarget
377
+ : null;
378
+ if (
379
+ input.existing.runMode === "reusable_session" &&
380
+ input.existing.reusableSessionId &&
381
+ nextRunMode === "existing_session"
382
+ ) {
383
+ throw new HTTPException(409, {
384
+ message:
385
+ "cannot target an existing session after this task created its reusable session; create a new task",
386
+ });
387
+ }
165
388
  if (input.payload.name !== undefined) {
166
389
  update.name = trimmedScheduledTaskName(input.payload.name);
167
390
  }
@@ -255,6 +478,7 @@ export async function validatedScheduledTaskUpdate(input: {
255
478
  input.db,
256
479
  input.existing.workspaceId,
257
480
  input.settings,
481
+ { subjectId: input.grant.subjectId },
258
482
  );
259
483
  const personalConnectionDelegations = await freezePersonalConnectionDelegations({
260
484
  db: input.db,
@@ -278,6 +502,43 @@ export async function validatedScheduledTaskUpdate(input: {
278
502
  }
279
503
  update.personalConnectionDelegations = personalConnectionDelegations;
280
504
  }
505
+ if (
506
+ existingTarget &&
507
+ (nextRunMode !== "existing_session" || nextTargetSessionId !== existingTarget)
508
+ ) {
509
+ await validateScheduledTaskTarget({
510
+ db: input.db,
511
+ sessionAuthorization: input.sessionAuthorization,
512
+ authorizationSurface: input.authorizationSurface,
513
+ grant: input.grant,
514
+ targetSessionId: existingTarget,
515
+ runMode: "existing_session",
516
+ variableSetId: input.existing.variableSetId,
517
+ rigId: input.existing.rigId,
518
+ agentConfig: input.existing.agentConfig,
519
+ });
520
+ }
521
+ await validateScheduledTaskTarget({
522
+ db: input.db,
523
+ sessionAuthorization: input.sessionAuthorization,
524
+ authorizationSurface: input.authorizationSurface,
525
+ grant: input.grant,
526
+ targetSessionId: nextTargetSessionId,
527
+ runMode: nextRunMode,
528
+ variableSetId:
529
+ input.payload.variableSetId !== undefined
530
+ ? input.payload.variableSetId
531
+ : input.existing.variableSetId,
532
+ rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
533
+ agentConfig: update.agentConfig ?? input.existing.agentConfig,
534
+ });
535
+ if (
536
+ input.payload.targetSessionId !== undefined ||
537
+ input.existing.runMode === "existing_session" ||
538
+ nextRunMode === "existing_session"
539
+ ) {
540
+ update.targetSessionId = nextTargetSessionId;
541
+ }
281
542
  return update;
282
543
  }
283
544
 
@@ -323,15 +584,85 @@ export async function restoreScheduledTask(
323
584
  schedule: task.schedule,
324
585
  runMode: task.runMode,
325
586
  overlapPolicy: task.overlapPolicy,
587
+ action: task.action,
326
588
  agentConfig: task.agentConfig,
327
589
  personalConnectionDelegations: previous.personalConnectionDelegations,
328
- reusableSessionId: task.reusableSessionId,
590
+ ...(task.runMode === "existing_session"
591
+ ? { targetSessionId: task.targetSessionId }
592
+ : { reusableSessionId: task.reusableSessionId }),
329
593
  variableSetId: task.variableSetId,
330
594
  rigId: task.rigId,
331
595
  metadata: task.metadata,
332
596
  });
333
597
  }
334
598
 
599
+ async function validateKnowledgeSourceSyncAction(input: {
600
+ db: Database;
601
+ grant: AccessGrant;
602
+ action: KnowledgeSourceSyncAction;
603
+ }): Promise<void> {
604
+ if (input.action.initiatingSubjectId !== input.grant.subjectId) {
605
+ throw new HTTPException(403, {
606
+ message: "knowledge source sync must preserve the exact initiating subject",
607
+ });
608
+ }
609
+ if (input.action.connection.ownerSubjectId !== input.grant.subjectId) {
610
+ throw new HTTPException(403, {
611
+ message: "knowledge source connection must belong to the initiating subject",
612
+ });
613
+ }
614
+ const resolved = await getKnowledgeSourceForSyncAuthority(input.db, {
615
+ accountId: input.grant.accountId,
616
+ workspaceId: input.grant.workspaceId,
617
+ sourceId: input.action.sourceId,
618
+ initiatingSubjectId: input.grant.subjectId,
619
+ });
620
+ if (!resolved || resolved.source.lifecycleState !== "active") {
621
+ throw new HTTPException(404, { message: "knowledge source not found" });
622
+ }
623
+ if (
624
+ resolved.source.syncGeneration !== input.action.sourceGeneration ||
625
+ resolved.source.lifecycleGeneration !== input.action.sourceLifecycleGeneration ||
626
+ scopedKnowledgeScopeKey(resolved.source.scope) !==
627
+ scopedKnowledgeScopeKey(input.action.destination)
628
+ ) {
629
+ throw new HTTPException(409, {
630
+ message: "knowledge source authority or generation changed",
631
+ });
632
+ }
633
+ const connection = await getConnectionMetadata(
634
+ input.db,
635
+ input.grant.workspaceId,
636
+ input.action.connection.connectionId,
637
+ input.grant.subjectId,
638
+ );
639
+ if (
640
+ !connection ||
641
+ connection.accountId !== input.grant.accountId ||
642
+ connection.workspaceId !== input.grant.workspaceId ||
643
+ connection.subjectId !== input.action.connection.ownerSubjectId ||
644
+ connection.version !== input.action.connection.connectionVersion ||
645
+ connection.providerDomain.toLowerCase() !==
646
+ input.action.connection.providerDomain.toLowerCase() ||
647
+ connection.kind !== input.action.connection.kind ||
648
+ connection.status !== "active"
649
+ ) {
650
+ throw new HTTPException(409, {
651
+ message: "knowledge source connection authority changed or requires reconnect",
652
+ });
653
+ }
654
+ }
655
+
656
+ export class ScheduledTaskSyncError extends Error {
657
+ readonly persistenceRestored: boolean;
658
+
659
+ constructor(cause: unknown, persistenceRestored: boolean) {
660
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
661
+ this.name = "ScheduledTaskSyncError";
662
+ this.persistenceRestored = persistenceRestored;
663
+ }
664
+ }
665
+
335
666
  export async function syncCreatedScheduledTask(input: {
336
667
  db: Database;
337
668
  workflowClient: SessionWorkflowClient;
@@ -340,10 +671,13 @@ export async function syncCreatedScheduledTask(input: {
340
671
  try {
341
672
  await input.workflowClient.syncScheduledTask({ task: input.task });
342
673
  } catch (error) {
343
- await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id).catch(
344
- () => undefined,
345
- );
346
- throw error;
674
+ let persistenceRestored = true;
675
+ try {
676
+ await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id);
677
+ } catch {
678
+ persistenceRestored = false;
679
+ }
680
+ throw new ScheduledTaskSyncError(error, persistenceRestored);
347
681
  }
348
682
  }
349
683
 
@@ -356,8 +690,13 @@ export async function syncUpdatedScheduledTask(input: {
356
690
  try {
357
691
  await input.workflowClient.syncScheduledTask({ task: input.task });
358
692
  } catch (error) {
359
- await restoreScheduledTask(input.db, input.previous).catch(() => undefined);
360
- throw error;
693
+ let persistenceRestored = true;
694
+ try {
695
+ await restoreScheduledTask(input.db, input.previous);
696
+ } catch {
697
+ persistenceRestored = false;
698
+ }
699
+ throw new ScheduledTaskSyncError(error, persistenceRestored);
361
700
  }
362
701
  }
363
702
 
@@ -433,6 +772,7 @@ async function validateScheduledTaskAgentConfig(input: {
433
772
  input.db,
434
773
  input.workspaceId,
435
774
  input.settings,
775
+ { subjectId: input.grant.subjectId },
436
776
  );
437
777
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
438
778
  // A task whose creator did not choose tools gets the workspace's enabled
@@ -169,8 +169,11 @@ export async function workspaceSessionToolPolicyServerIds(
169
169
  db: Database,
170
170
  workspaceId: string,
171
171
  settings: Settings,
172
+ subjectId?: string,
172
173
  ): Promise<string[]> {
173
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
174
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
175
+ ...(subjectId ? { subjectId } : {}),
176
+ });
174
177
  return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
175
178
  }
176
179
 
@@ -179,8 +182,11 @@ export async function workspaceSessionToolPolicyDefaultServerIds(
179
182
  db: Database,
180
183
  workspaceId: string,
181
184
  settings: Settings,
185
+ subjectId?: string,
182
186
  ): Promise<string[]> {
183
- const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
187
+ const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings, {
188
+ ...(subjectId ? { subjectId } : {}),
189
+ });
184
190
  return defaultSessionMcpServerIds(runtimeSettings.mcpServers);
185
191
  }
186
192