@opengeni/core 1.0.1 → 2.1.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 +151 -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
@@ -1,8 +1,9 @@
1
- import type { Settings } from "@opengeni/config";
1
+ import { resolveFirstPartyMcpToolPolicy, type Settings } from "@opengeni/config";
2
2
  import type {
3
3
  AccessGrant,
4
4
  McpPersonalConnectionDelegation,
5
5
  KnowledgeSourceSyncAction,
6
+ Permission,
6
7
  ScheduledTask,
7
8
  ScheduledTaskAgentConfig,
8
9
  Session,
@@ -12,7 +13,11 @@ import type {
12
13
  UpdateScheduledTaskRequest as UpdateScheduledTaskPayload,
13
14
  XaiProviderAccountAuthoritySnapshotV1,
14
15
  } from "@opengeni/contracts";
15
- import { OPENGENI_SLACK_BOT_SESSION_METADATA_KEY } from "@opengeni/contracts";
16
+ import {
17
+ DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
18
+ OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
19
+ resolveWorkspaceSessionToolDefaults,
20
+ } from "@opengeni/contracts";
16
21
  import {
17
22
  createScheduledTask,
18
23
  deleteScheduledTask,
@@ -21,17 +26,23 @@ import {
21
26
  getNestedAgentDepthDeploymentPolicy,
22
27
  getRig,
23
28
  getScheduledTask,
29
+ getScheduledTaskIncludingDeletedForUpdate,
24
30
  getScheduledTaskPersonalConnectionDelegations,
31
+ getScheduledTaskXaiProviderAccountAuthoritySnapshot,
25
32
  getSessionTurnXaiProviderAccountAuthoritySnapshot,
26
33
  getSession,
34
+ nestedPostgresSqlState,
27
35
  requireWorkspace,
28
36
  scopedKnowledgeScopeKey,
29
37
  updateScheduledTask,
38
+ withWorkspaceSubjectRls,
30
39
  resolveXaiProviderAccountAuthoritySnapshotForAcceptance,
31
40
  type Database,
41
+ type TemporalScheduleCleanupClaim,
32
42
  type UpdateScheduledTaskInput,
33
43
  } from "@opengeni/db";
34
44
  import { HTTPException } from "hono/http-exception";
45
+ import { isDeepStrictEqual } from "node:util";
35
46
  import { hasPermission, requirePermission } from "../access";
36
47
  import {
37
48
  requireSessionAuthorization,
@@ -45,7 +56,6 @@ import { validateVariableSetAttachment } from "./environments";
45
56
  import {
46
57
  freezePersonalConnectionDelegations,
47
58
  personalConnectionDelegationSourceForGrant,
48
- personalConnectionDelegationsEqual,
49
59
  } from "./personal-connection-delegations";
50
60
  import {
51
61
  assertWorkspaceModelPolicyAllows,
@@ -59,10 +69,11 @@ import {
59
69
  } from "./slack-bot";
60
70
  import {
61
71
  normalizeResources,
72
+ personalGitHubRepositoryResources,
62
73
  validateFileResources,
63
74
  validateGitHubRepositorySelection,
64
75
  validateToolRefs,
65
- withDefaultEnabledCapabilityMcpTools,
76
+ withWorkspaceDefaultMcpTools,
66
77
  } from "./resources";
67
78
 
68
79
  /**
@@ -85,6 +96,26 @@ export function scheduledTaskToolsProvided(rawPayload: unknown): boolean {
85
96
  );
86
97
  }
87
98
 
99
+ export function scheduledConnectionSurfaceEligibility(
100
+ settings: Settings,
101
+ target: Pick<Session, "firstPartyMcpTools" | "firstPartyMcpPermissions"> | null,
102
+ ): { googleDrivePublicationEnabled: boolean; atlassianEnabled: boolean } {
103
+ const tools = target?.firstPartyMcpTools ?? resolveFirstPartyMcpToolPolicy(settings).default;
104
+ const permissions = target?.firstPartyMcpPermissions?.length
105
+ ? target.firstPartyMcpPermissions
106
+ : DEFAULT_FIRST_PARTY_MCP_PERMISSIONS;
107
+ return {
108
+ googleDrivePublicationEnabled:
109
+ tools.includes("editable_artifact_export") &&
110
+ tools.includes("editable_artifact_export_status") &&
111
+ permissions.includes("artifacts:read") &&
112
+ permissions.includes("artifacts:publish"),
113
+ atlassianEnabled:
114
+ tools.some((tool) => tool.startsWith("atlassian_")) &&
115
+ permissions.includes("connections:read"),
116
+ };
117
+ }
118
+
88
119
  export async function createValidatedScheduledTask(input: {
89
120
  settings: Settings;
90
121
  db: Database;
@@ -147,22 +178,38 @@ export async function createValidatedScheduledTask(input: {
147
178
  // NOT that it has an active version now (that is a fire-time concern). RLS
148
179
  // makes a cross-workspace id indistinguishable from missing → both 422.
149
180
  if (!knowledgeAction && input.payload.rigId) {
150
- await requireScheduledTaskRig(input.db, input.grant.workspaceId, input.payload.rigId);
151
- }
152
- const personalConnectionDelegations = knowledgeAction
153
- ? []
154
- : await freezePersonalConnectionDelegations({
155
- db: input.db,
181
+ await requireScheduledTaskRig(
182
+ input.db,
183
+ {
184
+ accountId: input.grant.accountId,
156
185
  workspaceId: input.grant.workspaceId,
157
- settings: await settingsWithEnabledCapabilityMcpServers(
158
- input.db,
159
- input.grant.workspaceId,
160
- input.settings,
161
- { subjectId: input.grant.subjectId },
162
- ),
163
- tools: [...agentConfig.tools, { kind: "mcp", id: "opengeni" }],
164
- source: personalConnectionDelegationSourceForGrant(input.grant),
165
- });
186
+ subjectId: input.grant.subjectId,
187
+ },
188
+ input.payload.rigId,
189
+ );
190
+ }
191
+ const runtimeSettings = knowledgeAction
192
+ ? null
193
+ : await settingsWithEnabledCapabilityMcpServers(
194
+ input.db,
195
+ input.grant.workspaceId,
196
+ input.settings,
197
+ { subjectId: input.grant.subjectId },
198
+ );
199
+ const personalConnectionDelegations =
200
+ knowledgeAction || !runtimeSettings
201
+ ? []
202
+ : await freezePersonalConnectionDelegations({
203
+ db: input.db,
204
+ workspaceId: input.grant.workspaceId,
205
+ settings: runtimeSettings,
206
+ tools: [...agentConfig.tools, { kind: "mcp", id: "opengeni" }],
207
+ resources: agentConfig.resources,
208
+ source: personalConnectionDelegationSourceForGrant(input.grant),
209
+ authoritySelections: input.payload.connectionAuthorities,
210
+ rejectUnselectedActivatedConnections: true,
211
+ ...scheduledConnectionSurfaceEligibility(runtimeSettings, target),
212
+ });
166
213
  const creationInitiator = creationInitiatorForGrant(input.grant);
167
214
  const xaiProviderAccountAuthoritySnapshot: XaiProviderAccountAuthoritySnapshotV1 =
168
215
  creationInitiator.actor
@@ -176,28 +223,84 @@ export async function createValidatedScheduledTask(input: {
176
223
  workspaceId: input.grant.workspaceId,
177
224
  subjectId: input.grant.subjectId,
178
225
  });
179
- return await createScheduledTask(input.db, {
180
- id,
181
- accountId: input.grant.accountId,
182
- workspaceId: input.grant.workspaceId,
183
- name: trimmedScheduledTaskName(input.payload.name),
184
- status: input.payload.status,
185
- schedule: input.payload.schedule,
186
- temporalScheduleId: scheduledTaskTemporalScheduleId(id),
187
- runMode: input.payload.runMode,
188
- overlapPolicy: input.payload.overlapPolicy,
189
- action,
190
- agentConfig,
191
- ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
192
- ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
193
- createdByActor: creationInitiator.actor ?? null,
194
- personalConnectionDelegations,
195
- xaiProviderAccountAuthoritySnapshot,
196
- targetSessionId: target?.id ?? null,
197
- variableSetId: input.payload.variableSetId ?? null,
198
- rigId: input.payload.rigId ?? null,
199
- metadata: input.payload.metadata,
200
- });
226
+ return await withScheduledTaskAuthorityWriteErrors(() =>
227
+ createScheduledTask(input.db, {
228
+ id,
229
+ accountId: input.grant.accountId,
230
+ workspaceId: input.grant.workspaceId,
231
+ name: trimmedScheduledTaskName(input.payload.name),
232
+ status: input.payload.status,
233
+ schedule: input.payload.schedule,
234
+ temporalScheduleId: scheduledTaskTemporalScheduleId(id),
235
+ runMode: input.payload.runMode,
236
+ overlapPolicy: input.payload.overlapPolicy,
237
+ action,
238
+ agentConfig,
239
+ ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
240
+ ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
241
+ createdByActor: creationInitiator.actor ?? null,
242
+ personalConnectionDelegations,
243
+ xaiProviderAccountAuthoritySnapshot,
244
+ targetSessionId: target?.id ?? null,
245
+ variableSetId: input.payload.variableSetId ?? null,
246
+ rigId: input.payload.rigId ?? null,
247
+ metadata: input.payload.metadata,
248
+ }),
249
+ );
250
+ }
251
+
252
+ function nestedPostgresMessage(error: unknown): string | null {
253
+ const queue: unknown[] = [error];
254
+ const seen = new Set<unknown>();
255
+ while (queue.length > 0 && seen.size < 64) {
256
+ const current = queue.shift();
257
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
258
+ seen.add(current);
259
+ const record = current as Record<string, unknown>;
260
+ if (typeof record.code === "string" && typeof record.message === "string") {
261
+ return record.message;
262
+ }
263
+ for (const key of ["cause", "errors", "error", "originalError"]) {
264
+ const nested = record[key];
265
+ if (Array.isArray(nested)) queue.push(...nested);
266
+ else if (nested !== undefined) queue.push(nested);
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+
272
+ /**
273
+ * Scheduled-task authority writes fail closed inside SECURITY DEFINER seams
274
+ * (42501): a non-human writer delegating resources, an authorizer without
275
+ * active workspace membership, or a foreign human retaining another subject's
276
+ * grants. Those are caller-resolvable conflicts, not server faults.
277
+ */
278
+ export async function withScheduledTaskAuthorityWriteErrors<T>(run: () => Promise<T>): Promise<T> {
279
+ try {
280
+ return await run();
281
+ } catch (error) {
282
+ if (nestedPostgresSqlState(error) === "42501") {
283
+ const detail = nestedPostgresMessage(error);
284
+ throw new HTTPException(409, {
285
+ message: detail
286
+ ? `scheduled task authority denied: ${detail}`
287
+ : "scheduled task authority denied",
288
+ });
289
+ }
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ /** API/MCP-facing update that maps authority-write denials to 409. */
295
+ export async function updateScheduledTaskForApi(
296
+ db: Database,
297
+ workspaceId: string,
298
+ taskId: string,
299
+ update: UpdateScheduledTaskInput,
300
+ ): Promise<ScheduledTask> {
301
+ return await withScheduledTaskAuthorityWriteErrors(() =>
302
+ updateScheduledTask(db, workspaceId, taskId, update),
303
+ );
201
304
  }
202
305
 
203
306
  export async function validateScheduledTaskTarget(input: {
@@ -272,7 +375,7 @@ export async function validateScheduledTaskTarget(input: {
272
375
  message: "target session variableSet attachment does not match the scheduled task",
273
376
  });
274
377
  }
275
- if (input.rigId && input.rigId !== session.rigId) {
378
+ if ((session.rigId ?? null) !== (input.rigId ?? null)) {
276
379
  throw new HTTPException(422, {
277
380
  message: "target session rig does not match the scheduled task",
278
381
  });
@@ -313,14 +416,32 @@ export function scheduledTaskRunForGrant<T extends { sessionId: string | null }>
313
416
  return { ...run, sessionId: null };
314
417
  }
315
418
 
419
+ export function scheduledTaskAuthorityUpdateForGrant(
420
+ grant: AccessGrant,
421
+ ): Pick<
422
+ UpdateScheduledTaskInput,
423
+ | "refreshPersonalResourceAuthority"
424
+ | "authorityUpdatedBy"
425
+ | "authorityUpdatedByContext"
426
+ | "authorityUpdatedByActor"
427
+ > {
428
+ const writer = creationInitiatorForGrant(grant);
429
+ return {
430
+ refreshPersonalResourceAuthority: true,
431
+ ...(writer.initiator ? { authorityUpdatedBy: writer.initiator } : {}),
432
+ ...(writer.context ? { authorityUpdatedByContext: writer.context } : {}),
433
+ authorityUpdatedByActor: writer.actor ?? null,
434
+ };
435
+ }
436
+
316
437
  // Validate a scheduled task's rig reference: it must name a rig in the
317
438
  // workspace. A missing/cross-workspace id is a 422 (RLS-invisible == missing).
318
439
  async function requireScheduledTaskRig(
319
440
  db: Database,
320
- workspaceId: string,
441
+ access: { accountId: string; workspaceId: string; subjectId: string },
321
442
  rigId: string,
322
443
  ): Promise<void> {
323
- const rig = await getRig(db, workspaceId, rigId);
444
+ const rig = await getRig(db, access, rigId);
324
445
  if (!rig) {
325
446
  throw new HTTPException(422, { message: `unknown rigId: ${rigId}` });
326
447
  }
@@ -351,7 +472,8 @@ export async function validatedScheduledTaskUpdate(input: {
351
472
  input.payload.runMode !== undefined ||
352
473
  input.payload.targetSessionId !== undefined ||
353
474
  input.payload.variableSetId !== undefined ||
354
- input.payload.rigId !== undefined
475
+ input.payload.rigId !== undefined ||
476
+ input.payload.connectionAuthorities !== undefined
355
477
  ) {
356
478
  throw new HTTPException(422, {
357
479
  message: "knowledge source schedules do not accept agent/session configuration",
@@ -436,7 +558,7 @@ export async function validatedScheduledTaskUpdate(input: {
436
558
  if (input.existing.variableSetId !== null) {
437
559
  // Detaching is also an attachment change: it strips the secrets a
438
560
  // task's instructions were designed around.
439
- requirePermission(input.grant, "variable-sets:use");
561
+ requirePermission(input.grant, "variable-sets:attach");
440
562
  }
441
563
  update.variableSetId = null;
442
564
  } else {
@@ -450,11 +572,25 @@ export async function validatedScheduledTaskUpdate(input: {
450
572
  }
451
573
  }
452
574
  if (input.payload.rigId !== undefined) {
453
- // The rig binds fresh per fire, so changing it on a reusable-session task is
454
- // harmless for the LIVE session (which keeps its own frozen version) and
455
- // only affects subsequent new-session fires — no live-session guard needed.
575
+ if (
576
+ input.existing.runMode === "reusable_session" &&
577
+ input.existing.reusableSessionId !== null &&
578
+ input.payload.rigId !== input.existing.rigId
579
+ ) {
580
+ throw new HTTPException(409, {
581
+ message: "A reusable-session task cannot change rigId after materialization; recreate it",
582
+ });
583
+ }
456
584
  if (input.payload.rigId !== null) {
457
- await requireScheduledTaskRig(input.db, input.existing.workspaceId, input.payload.rigId);
585
+ await requireScheduledTaskRig(
586
+ input.db,
587
+ {
588
+ accountId: input.existing.accountId,
589
+ workspaceId: input.existing.workspaceId,
590
+ subjectId: input.grant.subjectId,
591
+ },
592
+ input.payload.rigId,
593
+ );
458
594
  }
459
595
  update.rigId = input.payload.rigId;
460
596
  }
@@ -490,33 +626,142 @@ export async function validatedScheduledTaskUpdate(input: {
490
626
  });
491
627
  }
492
628
  update.agentConfig = nextAgentConfig;
629
+ }
630
+ const nextAgentConfig = update.agentConfig ?? input.existing.agentConfig;
631
+ const authorityTargetChanged =
632
+ nextRunMode !== input.existing.runMode ||
633
+ nextTargetSessionId !== input.existing.targetSessionId ||
634
+ (input.payload.variableSetId !== undefined &&
635
+ input.payload.variableSetId !== input.existing.variableSetId) ||
636
+ (input.payload.rigId !== undefined && input.payload.rigId !== input.existing.rigId);
637
+ const materialExecutionChange =
638
+ authorityTargetChanged ||
639
+ input.payload.connectionAuthorities !== undefined ||
640
+ !isDeepStrictEqual(nextAgentConfig, input.existing.agentConfig) ||
641
+ (input.payload.action !== undefined &&
642
+ !isDeepStrictEqual(input.payload.action, input.existing.action)) ||
643
+ (input.payload.schedule !== undefined &&
644
+ !isDeepStrictEqual(input.payload.schedule, input.existing.schedule)) ||
645
+ (input.payload.overlapPolicy !== undefined &&
646
+ input.payload.overlapPolicy !== input.existing.overlapPolicy) ||
647
+ (input.payload.metadata !== undefined &&
648
+ !isDeepStrictEqual(input.payload.metadata, input.existing.metadata)) ||
649
+ (input.existing.status === "paused" && input.payload.status === "active");
650
+ const existingXaiAuthority = await getScheduledTaskXaiProviderAccountAuthoritySnapshot(
651
+ input.db,
652
+ input.existing.workspaceId,
653
+ input.existing.id,
654
+ );
655
+ if (
656
+ existingXaiAuthority.scope === "user" &&
657
+ materialExecutionChange &&
658
+ (input.existing.createdBy.kind !== "subject" ||
659
+ input.existing.createdBy.subjectId !== input.grant.subjectId)
660
+ ) {
661
+ throw new HTTPException(409, {
662
+ message: "changing a user-scoped xAI scheduled task requires the same causal human",
663
+ });
664
+ }
665
+ const existingDelegations = await getScheduledTaskPersonalConnectionDelegations(
666
+ input.db,
667
+ input.existing.workspaceId,
668
+ input.existing.id,
669
+ );
670
+ if (input.payload.connectionAuthorities === undefined) {
671
+ if (
672
+ materialExecutionChange &&
673
+ existingDelegations.some((delegation) => delegation.connectionType === "github_personal")
674
+ ) {
675
+ throw new HTTPException(409, {
676
+ message:
677
+ "material changes to a personal GitHub-authorized task require explicit connectionAuthorities",
678
+ });
679
+ }
680
+ if (
681
+ existingDelegations.length > 0 &&
682
+ !isDeepStrictEqual(nextAgentConfig.tools, input.existing.agentConfig.tools)
683
+ ) {
684
+ throw new HTTPException(409, {
685
+ message:
686
+ "changing tools on a connection-authorized task requires explicit connectionAuthorities",
687
+ });
688
+ }
689
+ if (existingDelegations.length > 0) {
690
+ if (
691
+ materialExecutionChange &&
692
+ existingDelegations.some(
693
+ (delegation) => delegation.ownerSubjectId !== input.grant.subjectId,
694
+ )
695
+ ) {
696
+ throw new HTTPException(409, {
697
+ message:
698
+ "preserving connection authority requires the same causal human; provide a new explicit selection",
699
+ });
700
+ }
701
+ if (authorityTargetChanged) {
702
+ update.cloneConnectionAuthorityFromRevision = input.existing.authorityRevision;
703
+ } else {
704
+ update.clonePersonalResourceAuthorityFromRevision = input.existing.authorityRevision;
705
+ }
706
+ }
707
+ } else if (input.payload.connectionAuthorities.length === 0) {
708
+ if (personalGitHubRepositoryResources(nextAgentConfig.resources).length > 0) {
709
+ throw new HTTPException(409, {
710
+ message:
711
+ "personal GitHub repository resources cannot be retained without connectionAuthorities",
712
+ });
713
+ }
714
+ update.personalConnectionDelegations = [];
715
+ } else {
493
716
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
494
717
  input.db,
495
- input.existing.workspaceId,
718
+ // Same object as the subject just below, and as the freeze call further
719
+ // down: the task was loaded with `getScheduledTask(db, grant.workspaceId,
720
+ // ...)`, so this is `input.existing.workspaceId`, sourced consistently.
721
+ input.grant.workspaceId,
496
722
  input.settings,
497
723
  { subjectId: input.grant.subjectId },
498
724
  );
499
- const personalConnectionDelegations = await freezePersonalConnectionDelegations({
725
+ const nextTarget = await validateScheduledTaskTarget({
500
726
  db: input.db,
501
- workspaceId: input.existing.workspaceId,
727
+ sessionAuthorization: input.sessionAuthorization,
728
+ authorizationSurface: input.authorizationSurface,
729
+ grant: input.grant,
730
+ targetSessionId: nextTargetSessionId,
731
+ runMode: nextRunMode,
732
+ variableSetId:
733
+ input.payload.variableSetId !== undefined
734
+ ? input.payload.variableSetId
735
+ : input.existing.variableSetId,
736
+ rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
737
+ agentConfig: nextAgentConfig,
738
+ });
739
+ update.personalConnectionDelegations = await freezePersonalConnectionDelegations({
740
+ db: input.db,
741
+ // Both halves of the authority question come from ONE object. The task was
742
+ // loaded with `getScheduledTask(db, grant.workspaceId, ...)`, so this is
743
+ // the same workspace as `input.existing.workspaceId`; taking it from the
744
+ // grant means the workspace can never drift from the `accountId` that
745
+ // `personalConnectionDelegationSourceForGrant` reads off the same grant.
746
+ workspaceId: input.grant.workspaceId,
502
747
  settings: runtimeSettings,
503
748
  tools: [...nextAgentConfig.tools, { kind: "mcp", id: "opengeni" }],
749
+ resources: nextAgentConfig.resources,
504
750
  source: personalConnectionDelegationSourceForGrant(input.grant),
751
+ authoritySelections: input.payload.connectionAuthorities,
752
+ rejectUnselectedActivatedConnections: true,
753
+ ...scheduledConnectionSurfaceEligibility(runtimeSettings, nextTarget),
505
754
  });
506
- if (input.existing.reusableSessionId && input.existing.runMode === "reusable_session") {
507
- const existingDelegations = await getScheduledTaskPersonalConnectionDelegations(
508
- input.db,
509
- input.existing.workspaceId,
510
- input.existing.id,
511
- );
512
- if (!personalConnectionDelegationsEqual(existingDelegations, personalConnectionDelegations)) {
513
- throw new HTTPException(409, {
514
- message:
515
- "cannot change personal MCP connections of a task with a live reusable session; recreate the task",
516
- });
517
- }
518
- }
519
- update.personalConnectionDelegations = personalConnectionDelegations;
755
+ }
756
+ if (
757
+ !materialExecutionChange &&
758
+ input.payload.connectionAuthorities === undefined &&
759
+ update.clonePersonalResourceAuthorityFromRevision === undefined
760
+ ) {
761
+ // Administrative lifecycle/name edits preserve the exact revision-bound
762
+ // causal human. They must not silently re-authorize retained Variable Set,
763
+ // Rig, Connection, or xAI authority under the manager performing the edit.
764
+ update.clonePersonalResourceAuthorityFromRevision = input.existing.authorityRevision;
520
765
  }
521
766
  if (
522
767
  existingTarget &&
@@ -551,10 +796,15 @@ export async function validatedScheduledTaskUpdate(input: {
551
796
  if (
552
797
  input.payload.targetSessionId !== undefined ||
553
798
  input.existing.runMode === "existing_session" ||
554
- nextRunMode === "existing_session"
799
+ nextRunMode === "existing_session" ||
800
+ (input.existing.runMode === "reusable_session" && nextRunMode !== "reusable_session")
555
801
  ) {
556
802
  update.targetSessionId = nextTargetSessionId;
557
803
  }
804
+ Object.assign(update, scheduledTaskAuthorityUpdateForGrant(input.grant));
805
+ if (update.clonePersonalResourceAuthorityFromRevision !== undefined) {
806
+ update.refreshPersonalResourceAuthority = false;
807
+ }
558
808
  return update;
559
809
  }
560
810
 
@@ -609,6 +859,7 @@ export async function restoreScheduledTask(
609
859
  variableSetId: task.variableSetId,
610
860
  rigId: task.rigId,
611
861
  metadata: task.metadata,
862
+ clonePersonalResourceAuthorityFromRevision: task.authorityRevision,
612
863
  });
613
864
  }
614
865
 
@@ -679,6 +930,73 @@ export class ScheduledTaskSyncError extends Error {
679
930
  }
680
931
  }
681
932
 
933
+ /**
934
+ * One deletion lifecycle shared by HTTP and first-party MCP adapters.
935
+ * Connector authorization is proven before the one-way tombstone. The same
936
+ * transaction detaches reclaimable task resources and persists both external
937
+ * cleanup obligations; best-effort processing is only an acceleration of that
938
+ * durable receipt.
939
+ */
940
+ export async function deleteScheduledTaskLifecycle(input: {
941
+ db: Database;
942
+ workspaceId: string;
943
+ taskId: string;
944
+ subjectId: string;
945
+ preflightConnectorAuthorization: (task: ScheduledTask) => Promise<void>;
946
+ cleanupConnectorAuthorization: (db: Database, task: ScheduledTask) => Promise<void>;
947
+ processCleanupClaims?: (claims: readonly TemporalScheduleCleanupClaim[]) => Promise<void>;
948
+ }): Promise<{
949
+ task: ScheduledTask;
950
+ changed: boolean;
951
+ cleanup: TemporalScheduleCleanupClaim | null;
952
+ }> {
953
+ const result = await withWorkspaceSubjectRls(
954
+ input.db,
955
+ input.workspaceId,
956
+ input.subjectId,
957
+ async (tx) => {
958
+ const task = await getScheduledTaskIncludingDeletedForUpdate(
959
+ tx,
960
+ input.workspaceId,
961
+ input.taskId,
962
+ );
963
+ if (!task) throw new HTTPException(404, { message: "Scheduled task not found" });
964
+ const wasLive = task.deletedAt === null;
965
+ if (
966
+ task.action.kind === "knowledge_source_sync" &&
967
+ (task.action.initiatingSubjectId !== input.subjectId ||
968
+ task.action.connection.ownerSubjectId !== input.subjectId)
969
+ ) {
970
+ throw new HTTPException(403, {
971
+ message: "knowledge source schedule requires the exact initiating subject",
972
+ });
973
+ }
974
+ if (wasLive && task.action.kind === "knowledge_source_sync") {
975
+ await input.preflightConnectorAuthorization(task);
976
+ }
977
+ const deletion =
978
+ task.action.kind === "knowledge_source_sync"
979
+ ? await (async () => {
980
+ await input.cleanupConnectorAuthorization(tx, task);
981
+ return await deleteScheduledTask(tx, input.workspaceId, task.id, {
982
+ connectorCleanupSubjectId: input.subjectId,
983
+ connectorCleanupCompleted: true,
984
+ expectedAuthorityRevision: task.authorityRevision,
985
+ expectedExecutionDigest: task.executionDigest,
986
+ });
987
+ })()
988
+ : await deleteScheduledTask(tx, input.workspaceId, task.id);
989
+ return { task, deletion };
990
+ },
991
+ );
992
+ const { task, deletion } = result;
993
+ const { cleanup, changed } = deletion;
994
+ if (cleanup && input.processCleanupClaims) {
995
+ await input.processCleanupClaims([cleanup]);
996
+ }
997
+ return { task, changed, cleanup };
998
+ }
999
+
682
1000
  export async function syncCreatedScheduledTask(input: {
683
1001
  db: Database;
684
1002
  workflowClient: SessionWorkflowClient;
@@ -689,6 +1007,9 @@ export async function syncCreatedScheduledTask(input: {
689
1007
  } catch (error) {
690
1008
  let persistenceRestored = true;
691
1009
  try {
1010
+ // Creation rollback removes only the failed schedule. Connector desired
1011
+ // state remains authoritative so a later connector save can rematerialize
1012
+ // it; this is intentionally not the user-requested deletion lifecycle.
692
1013
  await deleteScheduledTask(input.db, input.task.workspaceId, input.task.id);
693
1014
  } catch {
694
1015
  persistenceRestored = false;
@@ -791,15 +1112,23 @@ async function validateScheduledTaskAgentConfig(input: {
791
1112
  { subjectId: input.grant.subjectId },
792
1113
  );
793
1114
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
794
- // A task whose creator did not choose tools gets the workspace's enabled
795
- // capability MCP servers, exactly like a session created without a tools
796
- // key. Scheduled runs are sessions too; "no MCP servers at all" was a trap
797
- // every pack/template instantiation path kept falling into (a maintenance
798
- // task that cannot reach its workspace's notebook MCP cannot do its job).
1115
+ const workspace = await requireWorkspace(input.db, input.workspaceId);
1116
+ const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
1117
+ // A task whose creator did not choose tools gets the workspace's exact
1118
+ // session defaults (or the deployment compatibility default), exactly like
1119
+ // a session created without a tools key. Scheduled runs are sessions too;
1120
+ // "no MCP servers at all" was a trap every pack/template instantiation path
1121
+ // kept falling into (a maintenance task that cannot reach its workspace's
1122
+ // notebook MCP cannot do its job).
799
1123
  const tools =
800
1124
  (input.toolsProvided ?? true)
801
1125
  ? requestedTools
802
- : withDefaultEnabledCapabilityMcpTools(requestedTools, input.settings, runtimeSettings);
1126
+ : withWorkspaceDefaultMcpTools(
1127
+ requestedTools,
1128
+ input.settings,
1129
+ runtimeSettings,
1130
+ workspaceSessionToolDefaults,
1131
+ );
803
1132
  const prompt = input.payload.agentConfig.prompt.trim();
804
1133
  if (!prompt) {
805
1134
  throw new HTTPException(422, { message: "scheduled task prompt is required" });
@@ -830,7 +1159,6 @@ async function validateScheduledTaskAgentConfig(input: {
830
1159
  }
831
1160
  const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
832
1161
  if (requestedMaxDepth !== undefined) {
833
- const workspace = await requireWorkspace(input.db, input.workspaceId);
834
1162
  const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
835
1163
  const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
836
1164
  const inheritedMaxDepth =
@@ -846,13 +1174,106 @@ async function validateScheduledTaskAgentConfig(input: {
846
1174
  });
847
1175
  }
848
1176
  }
849
- return {
1177
+ const validated = {
850
1178
  ...input.payload.agentConfig,
851
1179
  ...(model === undefined || model === null ? {} : { model }),
852
1180
  prompt,
853
1181
  resources,
854
1182
  tools,
855
1183
  };
1184
+ validateIncidentTelemetryPreflightSelection(input.settings, validated);
1185
+ return validated;
1186
+ }
1187
+
1188
+ /**
1189
+ * Static incident-telemetry admission validates only already-selected task
1190
+ * authority. It never discovers a resource, reads a variable value, probes a
1191
+ * provider, or treats ambient worker credentials as responder capability.
1192
+ * Mutable rig/variable-set metadata is revalidated again at dispatch.
1193
+ */
1194
+ export function validateIncidentTelemetryPreflightSelection(
1195
+ settings: Pick<Settings, "allowedFirstPartyMcpTools" | "defaultFirstPartyMcpTools">,
1196
+ agentConfig: ScheduledTaskAgentConfig,
1197
+ ): void {
1198
+ const executionClass = agentConfig.executionClass;
1199
+ const preflight = agentConfig.incidentTelemetryPreflight;
1200
+ if (executionClass === undefined && preflight === undefined) return;
1201
+ if (executionClass !== "incident_telemetry" || !preflight) {
1202
+ throw new HTTPException(422, {
1203
+ message:
1204
+ "executionClass=incident_telemetry and incidentTelemetryPreflight must be configured together",
1205
+ });
1206
+ }
1207
+
1208
+ for (const required of preflight.requiredResources) {
1209
+ if (!agentConfig.resources.some((selected) => isDeepStrictEqual(selected, required))) {
1210
+ throw new HTTPException(422, {
1211
+ message: "incidentTelemetryPreflight.requiredResources must be exact selected resources",
1212
+ });
1213
+ }
1214
+ }
1215
+
1216
+ const selectedMcpServerIds = new Set(agentConfig.tools.map((tool) => tool.id));
1217
+ // Scheduled dispatch always attaches the first-party OpenGeni MCP server.
1218
+ selectedMcpServerIds.add("opengeni");
1219
+ if (preflight.requiredMcpServerIds.some((id) => !selectedMcpServerIds.has(id))) {
1220
+ throw new HTTPException(422, {
1221
+ message: "incidentTelemetryPreflight.requiredMcpServerIds must be exact selected MCP servers",
1222
+ });
1223
+ }
1224
+
1225
+ const selectedFirstPartyTools = new Set(resolveFirstPartyMcpToolPolicy(settings).default);
1226
+ if (preflight.requiredFirstPartyMcpTools.some((tool) => !selectedFirstPartyTools.has(tool))) {
1227
+ throw new HTTPException(422, {
1228
+ message:
1229
+ "incidentTelemetryPreflight.requiredFirstPartyMcpTools must be present in the selected first-party tool policy",
1230
+ });
1231
+ }
1232
+
1233
+ const selectedFirstPartyPermissions = new Set<Permission>(DEFAULT_FIRST_PARTY_MCP_PERMISSIONS);
1234
+ if (
1235
+ (preflight.requiredFirstPartyMcpPermissions ?? []).some(
1236
+ (permission) => !selectedFirstPartyPermissions.has(permission),
1237
+ )
1238
+ ) {
1239
+ throw new HTTPException(422, {
1240
+ message:
1241
+ "incidentTelemetryPreflight.requiredFirstPartyMcpPermissions must be present in the scheduled responder permission policy",
1242
+ });
1243
+ }
1244
+
1245
+ const route = preflight.dataSource.route;
1246
+ if (route.kind === "mcp" && !selectedMcpServerIds.has(route.serverId)) {
1247
+ throw new HTTPException(422, {
1248
+ message: "incidentTelemetryPreflight.dataSource.route must use a selected MCP server",
1249
+ });
1250
+ }
1251
+ if (route.kind === "first_party" && !selectedFirstPartyTools.has(route.tool)) {
1252
+ throw new HTTPException(422, {
1253
+ message: "incidentTelemetryPreflight.dataSource.route must use a selected first-party tool",
1254
+ });
1255
+ }
1256
+ if (route.kind === "variable_set") {
1257
+ const declaredSets = new Set(preflight.requiredVariableSetNames);
1258
+ const declaredVariables = new Set(preflight.requiredVariableNames);
1259
+ if (
1260
+ !declaredSets.has(route.variableSetName) ||
1261
+ route.variableNames.some((name) => !declaredVariables.has(name))
1262
+ ) {
1263
+ throw new HTTPException(422, {
1264
+ message:
1265
+ "incidentTelemetryPreflight.dataSource.route variable metadata must be declared as required",
1266
+ });
1267
+ }
1268
+ }
1269
+ if (route.kind === "rig_credential_hook") {
1270
+ if (!preflight.requiredRig?.credentialHookIds.includes(route.credentialHookId)) {
1271
+ throw new HTTPException(422, {
1272
+ message:
1273
+ "incidentTelemetryPreflight.dataSource.route rig hook must be declared as required",
1274
+ });
1275
+ }
1276
+ }
856
1277
  }
857
1278
 
858
1279
  function validateScheduledTaskSchedule(schedule: ScheduledTask["schedule"]): void {