@opengeni/core 0.28.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.
- package/dist/access/index.d.ts +37 -0
- package/dist/application/new-session-drafts.d.ts +8 -1
- package/dist/application/session-commands.d.ts +18 -11
- package/dist/application/session-tenancy.d.ts +18 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/{chunk-6WKPWE5S.js → chunk-H7X52RI2.js} +27 -18
- package/dist/chunk-H7X52RI2.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-VCZFFEPB.js} +21 -9
- package/dist/chunk-VCZFFEPB.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-ZYCNLYGH.js} +29 -27
- package/dist/chunk-ZYCNLYGH.js.map +1 -0
- package/dist/dependencies.d.ts +12 -1
- package/dist/domain/automations.d.ts +51 -0
- package/dist/domain/company-brain-governed-writes.d.ts +70 -0
- package/dist/domain/company-profile-agent-admin.d.ts +24 -0
- package/dist/domain/connection-authority.d.ts +97 -0
- package/dist/domain/connection-authority.js +169 -0
- package/dist/domain/connection-authority.js.map +1 -0
- package/dist/domain/editable-artifacts/service.d.ts +1 -2
- package/dist/domain/environments.d.ts +1 -1
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/governed-learning-activation.d.ts +16 -0
- package/dist/domain/governed-learning-evaluator.d.ts +6 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/memory-slack-delivery.d.ts +1 -4
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/packs.d.ts +5 -1
- package/dist/domain/personal-connection-delegations.d.ts +44 -2
- package/dist/domain/pr-review.d.ts +149 -0
- package/dist/domain/remember.d.ts +69 -0
- package/dist/domain/resources.d.ts +23 -2
- package/dist/domain/scheduled-tasks.d.ts +43 -2
- package/dist/domain/session-tool-policy.d.ts +1 -1
- package/dist/domain/sessions.d.ts +43 -9
- package/dist/editable-artifact-live/application.d.ts +10 -2
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +0 -1
- package/dist/editable-artifact-live/ticket.d.ts +0 -1
- package/dist/editable-artifact-live/types.d.ts +5 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +2 -4
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4973 -1243
- package/dist/index.js.map +1 -1
- package/dist/rigs/index.d.ts +22 -5
- package/dist/sandbox/fleet.d.ts +34 -6
- package/dist/sandbox/routing.d.ts +15 -1
- package/dist/sandbox/runtime-settings.d.ts +39 -0
- package/package.json +16 -11
- package/src/access/index.ts +151 -31
- package/src/application/new-session-drafts.ts +32 -5
- package/src/application/session-commands.ts +305 -71
- package/src/application/session-tenancy.ts +321 -0
- package/src/application/user-resource-grants.ts +140 -0
- package/src/billing/limits.ts +25 -15
- package/src/dependencies.ts +12 -1
- package/src/domain/automations.ts +196 -0
- package/src/domain/capabilities.ts +7 -1
- package/src/domain/company-brain-governed-writes.ts +330 -0
- package/src/domain/company-profile-agent-admin.ts +55 -0
- package/src/domain/connection-authority.ts +329 -0
- package/src/domain/editable-artifacts/agent-application.ts +17 -9
- package/src/domain/editable-artifacts/service.ts +12 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +16 -3
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/governed-learning-activation.ts +44 -0
- package/src/domain/governed-learning-evaluator.ts +49 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights.ts +9 -0
- package/src/domain/memory-slack-delivery.ts +0 -30
- package/src/domain/organization-membership-lifecycle.ts +9 -0
- package/src/domain/packs.ts +106 -3
- package/src/domain/personal-connection-delegations.ts +597 -42
- package/src/domain/pr-review.ts +671 -0
- package/src/domain/remember.ts +549 -0
- package/src/domain/resources.ts +144 -6
- package/src/domain/scheduled-tasks.ts +504 -77
- package/src/domain/session-tool-policy.ts +12 -9
- package/src/domain/sessions.ts +629 -176
- package/src/editable-artifact-live/application.ts +21 -3
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +13 -7
- package/src/editable-artifact-live/ticket.ts +1 -5
- package/src/editable-artifact-live/types.ts +6 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +13 -1
- package/src/rigs/index.ts +165 -48
- package/src/sandbox/fleet.ts +376 -30
- package/src/sandbox/routing.ts +91 -29
- package/src/sandbox/runtime-settings.ts +274 -0
- package/src/session-authorization.ts +17 -49
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/src/domain/durable-learning-slack-publication.ts +0 -202
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 (
|
|
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,
|
|
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:
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
725
|
+
const nextTarget = await validateScheduledTaskTarget({
|
|
500
726
|
db: input.db,
|
|
501
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
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
|
-
:
|
|
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" });
|
|
@@ -813,7 +1142,13 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
813
1142
|
if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
|
|
814
1143
|
throw new HTTPException(503, { message: "object storage is not configured" });
|
|
815
1144
|
}
|
|
816
|
-
await validateFileResources(
|
|
1145
|
+
await validateFileResources(
|
|
1146
|
+
input.db,
|
|
1147
|
+
input.grant.accountId,
|
|
1148
|
+
input.workspaceId,
|
|
1149
|
+
input.grant.subjectId,
|
|
1150
|
+
resources,
|
|
1151
|
+
);
|
|
817
1152
|
if (input.payload.agentConfig.slackBotConnectionId) {
|
|
818
1153
|
await validateOpenGeniSlackBotConnectionSelection(
|
|
819
1154
|
input.db,
|
|
@@ -824,7 +1159,6 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
824
1159
|
}
|
|
825
1160
|
const requestedMaxDepth = input.payload.agentConfig.maxNestedAgentDepth;
|
|
826
1161
|
if (requestedMaxDepth !== undefined) {
|
|
827
|
-
const workspace = await requireWorkspace(input.db, input.workspaceId);
|
|
828
1162
|
const workspaceMaxDepth = workspace.settings.maxNestedAgentDepth;
|
|
829
1163
|
const deploymentPolicy = await getNestedAgentDepthDeploymentPolicy(input.db);
|
|
830
1164
|
const inheritedMaxDepth =
|
|
@@ -840,13 +1174,106 @@ async function validateScheduledTaskAgentConfig(input: {
|
|
|
840
1174
|
});
|
|
841
1175
|
}
|
|
842
1176
|
}
|
|
843
|
-
|
|
1177
|
+
const validated = {
|
|
844
1178
|
...input.payload.agentConfig,
|
|
845
1179
|
...(model === undefined || model === null ? {} : { model }),
|
|
846
1180
|
prompt,
|
|
847
1181
|
resources,
|
|
848
1182
|
tools,
|
|
849
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
|
+
}
|
|
850
1277
|
}
|
|
851
1278
|
|
|
852
1279
|
function validateScheduledTaskSchedule(schedule: ScheduledTask["schedule"]): void {
|