@sentry/junior-scheduler 0.68.0 → 0.70.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/index.d.ts +1 -1
- package/dist/index.js +73 -46
- package/dist/schedule-tools.d.ts +3 -12
- package/dist/store.d.ts +1 -1
- package/dist/types.d.ts +3 -9
- package/package.json +2 -2
- package/src/index.ts +0 -2
- package/src/plugin.ts +9 -11
- package/src/schedule-tools.ts +48 -56
- package/src/store.ts +42 -11
- package/src/types.ts +6 -11
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { createSchedulerPlugin, schedulerPlugin } from "./plugin";
|
|
|
2
2
|
export { buildScheduledTaskRunPrompt } from "./prompt";
|
|
3
3
|
export { createSlackScheduleCreateTaskTool, createSlackScheduleDeleteTaskTool, createSlackScheduleListTasksTool, createSlackScheduleRunTaskNowTool, createSlackScheduleUpdateTaskTool, type SchedulerToolContext, } from "./schedule-tools";
|
|
4
4
|
export { createSchedulerStore } from "./store";
|
|
5
|
-
export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess,
|
|
5
|
+
export type { ScheduledCalendarFrequency, ScheduledLocalTime, ScheduledRun, ScheduledRunStatus, ScheduledTask, ScheduledTaskConversationAccess, ScheduledTaskExecutionActor, ScheduledTaskPrincipal, ScheduledTaskRecurrence, ScheduledTaskSchedule, ScheduledTaskSpec, ScheduledTaskStatus, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,12 @@ function buildScheduledTaskRunPrompt(args) {
|
|
|
122
122
|
].join("\n");
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// src/store.ts
|
|
126
|
+
import {
|
|
127
|
+
agentPluginCredentialSubjectSchema,
|
|
128
|
+
destinationSchema
|
|
129
|
+
} from "@sentry/junior-plugin-api";
|
|
130
|
+
|
|
125
131
|
// src/cadence.ts
|
|
126
132
|
function parseScheduleTimestamp(value) {
|
|
127
133
|
const trimmed = value.trim();
|
|
@@ -650,8 +656,34 @@ function canFinishRun(run, startedAtMs) {
|
|
|
650
656
|
}
|
|
651
657
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
652
658
|
}
|
|
659
|
+
function parseStoredTask(value) {
|
|
660
|
+
if (!value || typeof value !== "object") {
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
const record = value;
|
|
664
|
+
const destination = destinationSchema.safeParse(record.destination);
|
|
665
|
+
if (!destination.success) {
|
|
666
|
+
return void 0;
|
|
667
|
+
}
|
|
668
|
+
const credentialSubject = record.credentialSubject === void 0 ? void 0 : agentPluginCredentialSubjectSchema.safeParse(record.credentialSubject);
|
|
669
|
+
if (credentialSubject && !credentialSubject.success) {
|
|
670
|
+
return void 0;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
...record,
|
|
674
|
+
destination: destination.data,
|
|
675
|
+
...credentialSubject ? { credentialSubject: credentialSubject.data } : {}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function requireStoredTask(task) {
|
|
679
|
+
const parsed = parseStoredTask(task);
|
|
680
|
+
if (!parsed) {
|
|
681
|
+
throw new Error("Scheduled task routing context is invalid.");
|
|
682
|
+
}
|
|
683
|
+
return parsed;
|
|
684
|
+
}
|
|
653
685
|
async function getTaskFromState(state, taskId) {
|
|
654
|
-
return await state.get(taskKey(taskId))
|
|
686
|
+
return parseStoredTask(await state.get(taskKey(taskId)));
|
|
655
687
|
}
|
|
656
688
|
async function listTasksFromState(state, indexKey) {
|
|
657
689
|
const ids = await getIndex(state, indexKey);
|
|
@@ -693,9 +725,10 @@ var PluginStateSchedulerStore = class {
|
|
|
693
725
|
this.state = state;
|
|
694
726
|
}
|
|
695
727
|
async saveTask(task) {
|
|
728
|
+
const next = requireStoredTask(task);
|
|
696
729
|
await withLock(this.state, taskLockKey(task.id), async () => {
|
|
697
|
-
const current = await this.state
|
|
698
|
-
await this.saveTaskRecord(
|
|
730
|
+
const current = await getTaskFromState(this.state, task.id);
|
|
731
|
+
await this.saveTaskRecord(next, current);
|
|
699
732
|
});
|
|
700
733
|
}
|
|
701
734
|
async saveTaskRecord(task, current) {
|
|
@@ -804,7 +837,7 @@ var PluginStateSchedulerStore = class {
|
|
|
804
837
|
}
|
|
805
838
|
async skipMissedRun(args) {
|
|
806
839
|
await withLock(this.state, taskLockKey(args.task.id), async () => {
|
|
807
|
-
const current = await this.state
|
|
840
|
+
const current = await getTaskFromState(this.state, args.task.id) ?? void 0;
|
|
808
841
|
if (!current || current.status !== "active" || getDueRunAtMs(current, args.nowMs) !== args.scheduledForMs) {
|
|
809
842
|
return;
|
|
810
843
|
}
|
|
@@ -933,7 +966,7 @@ var PluginStateSchedulerStore = class {
|
|
|
933
966
|
}
|
|
934
967
|
async updateTaskAfterRun(args) {
|
|
935
968
|
await withLock(this.state, taskLockKey(args.run.taskId), async () => {
|
|
936
|
-
const current = await this.state
|
|
969
|
+
const current = await getTaskFromState(this.state, args.run.taskId) ?? void 0;
|
|
937
970
|
if (!current || current.status === "deleted") {
|
|
938
971
|
return;
|
|
939
972
|
}
|
|
@@ -1015,7 +1048,9 @@ function createSchedulerOperationalStore(state) {
|
|
|
1015
1048
|
import { randomUUID } from "crypto";
|
|
1016
1049
|
import { Type } from "@sinclair/typebox";
|
|
1017
1050
|
import {
|
|
1018
|
-
AgentPluginToolInputError
|
|
1051
|
+
AgentPluginToolInputError,
|
|
1052
|
+
agentPluginCredentialSubjectSchema as agentPluginCredentialSubjectSchema2,
|
|
1053
|
+
destinationSchema as destinationSchema2
|
|
1019
1054
|
} from "@sentry/junior-plugin-api";
|
|
1020
1055
|
var TASK_ID_PREFIX = "sched";
|
|
1021
1056
|
var MAX_LISTED_TASKS = 50;
|
|
@@ -1024,21 +1059,27 @@ function throwToolInputError(error) {
|
|
|
1024
1059
|
throw new AgentPluginToolInputError(error);
|
|
1025
1060
|
}
|
|
1026
1061
|
function requireActiveDestination(context) {
|
|
1027
|
-
const
|
|
1028
|
-
if (!
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1062
|
+
const parsed = destinationSchema2.safeParse(context.destination);
|
|
1063
|
+
if (!parsed.success) {
|
|
1064
|
+
const destination = context.destination;
|
|
1065
|
+
const issues = parsed.error.issues;
|
|
1066
|
+
if (!destination || destination.platform !== "slack") {
|
|
1067
|
+
throwToolInputError("No active Slack destination is available.");
|
|
1068
|
+
}
|
|
1069
|
+
if (issues.some((issue) => issue.code === "unrecognized_keys")) {
|
|
1070
|
+
throwToolInputError(
|
|
1071
|
+
"Active Slack destination must not include unknown fields."
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
if (issues.some((issue) => issue.path[0] === "channelId")) {
|
|
1075
|
+
throwToolInputError("Active Slack destination channel is invalid.");
|
|
1076
|
+
}
|
|
1077
|
+
if (issues.some((issue) => issue.path[0] === "teamId")) {
|
|
1078
|
+
throwToolInputError("Active Slack destination workspace is invalid.");
|
|
1079
|
+
}
|
|
1080
|
+
throwToolInputError("No active Slack destination is available.");
|
|
1036
1081
|
}
|
|
1037
|
-
return
|
|
1038
|
-
platform: "slack",
|
|
1039
|
-
teamId: context.teamId,
|
|
1040
|
-
channelId
|
|
1041
|
-
};
|
|
1082
|
+
return parsed.data;
|
|
1042
1083
|
}
|
|
1043
1084
|
function requireRequester(context) {
|
|
1044
1085
|
const userId = context.requester?.userId?.trim();
|
|
@@ -1054,19 +1095,8 @@ function requireRequester(context) {
|
|
|
1054
1095
|
function tool(definition) {
|
|
1055
1096
|
return definition;
|
|
1056
1097
|
}
|
|
1057
|
-
function normalizeSlackConversationId(value) {
|
|
1058
|
-
if (!value) return void 0;
|
|
1059
|
-
const trimmed = value.trim();
|
|
1060
|
-
if (!trimmed) return void 0;
|
|
1061
|
-
if (!trimmed.startsWith("slack:")) return trimmed;
|
|
1062
|
-
const parts = trimmed.split(":");
|
|
1063
|
-
return parts[1]?.trim() || void 0;
|
|
1064
|
-
}
|
|
1065
1098
|
function isDmChannel(channelId) {
|
|
1066
|
-
return
|
|
1067
|
-
}
|
|
1068
|
-
function isSlackTeamId(value) {
|
|
1069
|
-
return /^T[A-Z0-9]+$/.test(value);
|
|
1099
|
+
return channelId.startsWith("D");
|
|
1070
1100
|
}
|
|
1071
1101
|
function getConversationAccess(destination) {
|
|
1072
1102
|
if (isDmChannel(destination.channelId)) {
|
|
@@ -1087,14 +1117,19 @@ function getCredentialSubject(args) {
|
|
|
1087
1117
|
if (!args.subject) {
|
|
1088
1118
|
return void 0;
|
|
1089
1119
|
}
|
|
1120
|
+
const subject = agentPluginCredentialSubjectSchema2.safeParse(args.subject);
|
|
1121
|
+
if (!subject.success) {
|
|
1122
|
+
throwToolInputError("Active Slack credential subject is invalid.");
|
|
1123
|
+
}
|
|
1090
1124
|
return {
|
|
1091
|
-
type:
|
|
1092
|
-
userId:
|
|
1093
|
-
allowedWhen:
|
|
1125
|
+
type: subject.data.type,
|
|
1126
|
+
userId: subject.data.userId,
|
|
1127
|
+
allowedWhen: subject.data.allowedWhen
|
|
1094
1128
|
};
|
|
1095
1129
|
}
|
|
1096
1130
|
function sameDestination(task, destination) {
|
|
1097
|
-
|
|
1131
|
+
const taskDestination = task.destination;
|
|
1132
|
+
return taskDestination.platform === "slack" && taskDestination.teamId === destination.teamId && taskDestination.channelId === destination.channelId;
|
|
1098
1133
|
}
|
|
1099
1134
|
async function getWritableTask(args) {
|
|
1100
1135
|
const destination = requireActiveDestination(args.context);
|
|
@@ -1500,18 +1535,10 @@ function shouldSkipRun(task, run) {
|
|
|
1500
1535
|
}
|
|
1501
1536
|
function createSchedulerToolContext(ctx) {
|
|
1502
1537
|
return {
|
|
1503
|
-
channelCapabilities: ctx.channelCapabilities ?? {
|
|
1504
|
-
canAddReactions: false,
|
|
1505
|
-
canCreateCanvas: false,
|
|
1506
|
-
canPostToChannel: false
|
|
1507
|
-
},
|
|
1508
|
-
channelId: ctx.channelId,
|
|
1509
1538
|
credentialSubject: ctx.credentialSubject,
|
|
1510
|
-
|
|
1539
|
+
destination: ctx.destination?.platform === "slack" ? ctx.destination : void 0,
|
|
1511
1540
|
requester: ctx.requester,
|
|
1512
1541
|
state: ctx.state,
|
|
1513
|
-
teamId: ctx.teamId,
|
|
1514
|
-
threadTs: ctx.threadTs,
|
|
1515
1542
|
userText: ctx.userText
|
|
1516
1543
|
};
|
|
1517
1544
|
}
|
|
@@ -1763,7 +1790,7 @@ function createSchedulerPlugin() {
|
|
|
1763
1790
|
legacyStatePrefixes: ["junior:scheduler"],
|
|
1764
1791
|
hooks: {
|
|
1765
1792
|
tools(ctx) {
|
|
1766
|
-
if (!ctx.
|
|
1793
|
+
if (!ctx.destination || ctx.destination.platform !== "slack" || !ctx.requester?.userId) {
|
|
1767
1794
|
return {};
|
|
1768
1795
|
}
|
|
1769
1796
|
const context = createSchedulerToolContext(ctx);
|
package/dist/schedule-tools.d.ts
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
import { type AgentPluginRequester, type AgentPluginState, type AgentPluginToolDefinition } from "@sentry/junior-plugin-api";
|
|
2
|
-
import type { ScheduledTaskCredentialSubject } from "./types";
|
|
1
|
+
import { type AgentPluginCredentialSubject, type Destination, type AgentPluginRequester, type AgentPluginState, type AgentPluginToolDefinition } from "@sentry/junior-plugin-api";
|
|
3
2
|
export interface SchedulerToolContext {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
canCreateCanvas: boolean;
|
|
7
|
-
canPostToChannel: boolean;
|
|
8
|
-
};
|
|
9
|
-
channelId?: string;
|
|
10
|
-
credentialSubject?: ScheduledTaskCredentialSubject;
|
|
11
|
-
messageTs?: string;
|
|
3
|
+
credentialSubject?: AgentPluginCredentialSubject;
|
|
4
|
+
destination?: Destination;
|
|
12
5
|
requester?: AgentPluginRequester;
|
|
13
6
|
state: AgentPluginState;
|
|
14
|
-
teamId?: string;
|
|
15
|
-
threadTs?: string;
|
|
16
7
|
userText?: string;
|
|
17
8
|
}
|
|
18
9
|
/** Create a tool that stores a scheduled task for the active Slack context. */
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AgentPluginReadState, type AgentPluginState } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
3
3
|
export interface SchedulerStore {
|
|
4
4
|
claimDueRun(args: {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentPluginCredentialSubject } from "@sentry/junior-plugin-api";
|
|
1
|
+
import type { AgentPluginCredentialSubject, Destination } from "@sentry/junior-plugin-api";
|
|
2
2
|
export type ScheduledTaskStatus = "active" | "paused" | "blocked" | "deleted";
|
|
3
3
|
export type ScheduledRunStatus = "pending" | "running" | "completed" | "failed" | "blocked" | "skipped";
|
|
4
4
|
export interface ScheduledTaskPrincipal {
|
|
@@ -14,16 +14,10 @@ export declare const SCHEDULED_TASK_SYSTEM_ACTOR: Readonly<{
|
|
|
14
14
|
type: "system";
|
|
15
15
|
id: string;
|
|
16
16
|
}>;
|
|
17
|
-
export interface ScheduledTaskDestination {
|
|
18
|
-
platform: "slack";
|
|
19
|
-
teamId: string;
|
|
20
|
-
channelId: string;
|
|
21
|
-
}
|
|
22
17
|
export interface ScheduledTaskConversationAccess {
|
|
23
18
|
audience: "direct" | "group" | "channel";
|
|
24
19
|
visibility: "private" | "public" | "unknown";
|
|
25
20
|
}
|
|
26
|
-
export type ScheduledTaskCredentialSubject = AgentPluginCredentialSubject;
|
|
27
21
|
export type ScheduledCalendarFrequency = "daily" | "weekly" | "monthly" | "yearly";
|
|
28
22
|
export interface ScheduledLocalTime {
|
|
29
23
|
hour: number;
|
|
@@ -52,8 +46,8 @@ export interface ScheduledTask {
|
|
|
52
46
|
createdAtMs: number;
|
|
53
47
|
createdBy: ScheduledTaskPrincipal;
|
|
54
48
|
conversationAccess?: ScheduledTaskConversationAccess;
|
|
55
|
-
credentialSubject?:
|
|
56
|
-
destination:
|
|
49
|
+
credentialSubject?: AgentPluginCredentialSubject;
|
|
50
|
+
destination: Destination;
|
|
57
51
|
executionActor?: ScheduledTaskExecutionActor;
|
|
58
52
|
lastRunAtMs?: number;
|
|
59
53
|
nextRunAtMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-scheduler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@sinclair/typebox": "^0.34.49",
|
|
26
|
-
"@sentry/junior-plugin-api": "0.
|
|
26
|
+
"@sentry/junior-plugin-api": "0.70.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.9.1",
|
package/src/index.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineJuniorPlugin,
|
|
3
|
+
type Destination,
|
|
3
4
|
type Dispatch,
|
|
4
5
|
type AgentPluginToolDefinition,
|
|
5
6
|
type PluginOperationalReportContent,
|
|
@@ -53,18 +54,11 @@ function createSchedulerToolContext(
|
|
|
53
54
|
ctx: ToolRegistrationHookContext,
|
|
54
55
|
): SchedulerToolContext {
|
|
55
56
|
return {
|
|
56
|
-
channelCapabilities: ctx.channelCapabilities ?? {
|
|
57
|
-
canAddReactions: false,
|
|
58
|
-
canCreateCanvas: false,
|
|
59
|
-
canPostToChannel: false,
|
|
60
|
-
},
|
|
61
|
-
channelId: ctx.channelId,
|
|
62
57
|
credentialSubject: ctx.credentialSubject,
|
|
63
|
-
|
|
58
|
+
destination:
|
|
59
|
+
ctx.destination?.platform === "slack" ? ctx.destination : undefined,
|
|
64
60
|
requester: ctx.requester,
|
|
65
61
|
state: ctx.state,
|
|
66
|
-
teamId: ctx.teamId,
|
|
67
|
-
threadTs: ctx.threadTs,
|
|
68
62
|
userText: ctx.userText,
|
|
69
63
|
};
|
|
70
64
|
}
|
|
@@ -189,7 +183,7 @@ function formatTimestamp(timestampMs: number | undefined): string {
|
|
|
189
183
|
: "none";
|
|
190
184
|
}
|
|
191
185
|
|
|
192
|
-
function destinationLabel(destination:
|
|
186
|
+
function destinationLabel(destination: Destination): string {
|
|
193
187
|
if (destination.channelId.startsWith("D")) {
|
|
194
188
|
return "Direct Message";
|
|
195
189
|
}
|
|
@@ -369,7 +363,11 @@ export function createSchedulerPlugin() {
|
|
|
369
363
|
legacyStatePrefixes: ["junior:scheduler"],
|
|
370
364
|
hooks: {
|
|
371
365
|
tools(ctx) {
|
|
372
|
-
if (
|
|
366
|
+
if (
|
|
367
|
+
!ctx.destination ||
|
|
368
|
+
ctx.destination.platform !== "slack" ||
|
|
369
|
+
!ctx.requester?.userId
|
|
370
|
+
) {
|
|
373
371
|
return {} as Record<string, AgentPluginToolDefinition<any>>;
|
|
374
372
|
}
|
|
375
373
|
const context = createSchedulerToolContext(ctx);
|
package/src/schedule-tools.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import {
|
|
4
4
|
AgentPluginToolInputError,
|
|
5
|
+
agentPluginCredentialSubjectSchema,
|
|
6
|
+
destinationSchema,
|
|
7
|
+
type AgentPluginCredentialSubject,
|
|
8
|
+
type Destination,
|
|
5
9
|
type AgentPluginRequester,
|
|
6
10
|
type AgentPluginState,
|
|
7
11
|
type AgentPluginToolDefinition,
|
|
@@ -14,26 +18,16 @@ import type {
|
|
|
14
18
|
ScheduledCalendarFrequency,
|
|
15
19
|
ScheduledTask,
|
|
16
20
|
ScheduledTaskConversationAccess,
|
|
17
|
-
ScheduledTaskCredentialSubject,
|
|
18
|
-
ScheduledTaskDestination,
|
|
19
21
|
ScheduledTaskPrincipal,
|
|
20
22
|
ScheduledTaskRecurrence,
|
|
21
23
|
ScheduledTaskStatus,
|
|
22
24
|
} from "./types";
|
|
23
25
|
|
|
24
26
|
export interface SchedulerToolContext {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
canCreateCanvas: boolean;
|
|
28
|
-
canPostToChannel: boolean;
|
|
29
|
-
};
|
|
30
|
-
channelId?: string;
|
|
31
|
-
credentialSubject?: ScheduledTaskCredentialSubject;
|
|
32
|
-
messageTs?: string;
|
|
27
|
+
credentialSubject?: AgentPluginCredentialSubject;
|
|
28
|
+
destination?: Destination;
|
|
33
29
|
requester?: AgentPluginRequester;
|
|
34
30
|
state: AgentPluginState;
|
|
35
|
-
teamId?: string;
|
|
36
|
-
threadTs?: string;
|
|
37
31
|
userText?: string;
|
|
38
32
|
}
|
|
39
33
|
|
|
@@ -41,29 +35,38 @@ const TASK_ID_PREFIX = "sched";
|
|
|
41
35
|
const MAX_LISTED_TASKS = 50;
|
|
42
36
|
const DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
|
|
43
37
|
|
|
38
|
+
type SchemaIssue = {
|
|
39
|
+
code: string;
|
|
40
|
+
path: readonly PropertyKey[];
|
|
41
|
+
};
|
|
42
|
+
|
|
44
43
|
function throwToolInputError(error: string): never {
|
|
45
44
|
throw new AgentPluginToolInputError(error);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
function requireActiveDestination(
|
|
49
|
-
context
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
function requireActiveDestination(context: SchedulerToolContext): Destination {
|
|
48
|
+
const parsed = destinationSchema.safeParse(context.destination);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
const destination = context.destination as Partial<Destination> | undefined;
|
|
51
|
+
const issues = parsed.error.issues as readonly SchemaIssue[];
|
|
52
|
+
if (!destination || destination.platform !== "slack") {
|
|
53
|
+
throwToolInputError("No active Slack destination is available.");
|
|
54
|
+
}
|
|
55
|
+
if (issues.some((issue) => issue.code === "unrecognized_keys")) {
|
|
56
|
+
throwToolInputError(
|
|
57
|
+
"Active Slack destination must not include unknown fields.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (issues.some((issue) => issue.path[0] === "channelId")) {
|
|
61
|
+
throwToolInputError("Active Slack destination channel is invalid.");
|
|
62
|
+
}
|
|
63
|
+
if (issues.some((issue) => issue.path[0] === "teamId")) {
|
|
64
|
+
throwToolInputError("Active Slack destination workspace is invalid.");
|
|
65
|
+
}
|
|
66
|
+
throwToolInputError("No active Slack destination is available.");
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
return
|
|
63
|
-
platform: "slack",
|
|
64
|
-
teamId: context.teamId,
|
|
65
|
-
channelId,
|
|
66
|
-
};
|
|
69
|
+
return parsed.data;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function requireRequester(
|
|
@@ -91,28 +94,12 @@ function tool<TInput = any>(
|
|
|
91
94
|
return definition;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
function normalizeSlackConversationId(
|
|
95
|
-
value: string | undefined,
|
|
96
|
-
): string | undefined {
|
|
97
|
-
if (!value) return undefined;
|
|
98
|
-
const trimmed = value.trim();
|
|
99
|
-
if (!trimmed) return undefined;
|
|
100
|
-
if (!trimmed.startsWith("slack:")) return trimmed;
|
|
101
|
-
|
|
102
|
-
const parts = trimmed.split(":");
|
|
103
|
-
return parts[1]?.trim() || undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
97
|
function isDmChannel(channelId: string): boolean {
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function isSlackTeamId(value: string): boolean {
|
|
111
|
-
return /^T[A-Z0-9]+$/.test(value);
|
|
98
|
+
return channelId.startsWith("D");
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
function getConversationAccess(
|
|
115
|
-
destination:
|
|
102
|
+
destination: Destination,
|
|
116
103
|
): ScheduledTaskConversationAccess {
|
|
117
104
|
if (isDmChannel(destination.channelId)) {
|
|
118
105
|
return { audience: "direct", visibility: "private" };
|
|
@@ -128,8 +115,8 @@ function getConversationAccess(
|
|
|
128
115
|
|
|
129
116
|
function getCredentialSubject(args: {
|
|
130
117
|
access: ScheduledTaskConversationAccess;
|
|
131
|
-
subject:
|
|
132
|
-
}):
|
|
118
|
+
subject: AgentPluginCredentialSubject | undefined;
|
|
119
|
+
}): AgentPluginCredentialSubject | undefined {
|
|
133
120
|
if (
|
|
134
121
|
args.access.audience !== "direct" ||
|
|
135
122
|
args.access.visibility !== "private"
|
|
@@ -139,21 +126,26 @@ function getCredentialSubject(args: {
|
|
|
139
126
|
if (!args.subject) {
|
|
140
127
|
return undefined;
|
|
141
128
|
}
|
|
129
|
+
const subject = agentPluginCredentialSubjectSchema.safeParse(args.subject);
|
|
130
|
+
if (!subject.success) {
|
|
131
|
+
throwToolInputError("Active Slack credential subject is invalid.");
|
|
132
|
+
}
|
|
142
133
|
return {
|
|
143
|
-
type:
|
|
144
|
-
userId:
|
|
145
|
-
allowedWhen:
|
|
134
|
+
type: subject.data.type,
|
|
135
|
+
userId: subject.data.userId,
|
|
136
|
+
allowedWhen: subject.data.allowedWhen,
|
|
146
137
|
};
|
|
147
138
|
}
|
|
148
139
|
|
|
149
140
|
function sameDestination(
|
|
150
141
|
task: ScheduledTask,
|
|
151
|
-
destination:
|
|
142
|
+
destination: Destination,
|
|
152
143
|
): boolean {
|
|
144
|
+
const taskDestination = task.destination;
|
|
153
145
|
return (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
146
|
+
taskDestination.platform === "slack" &&
|
|
147
|
+
taskDestination.teamId === destination.teamId &&
|
|
148
|
+
taskDestination.channelId === destination.channelId
|
|
157
149
|
);
|
|
158
150
|
}
|
|
159
151
|
|
package/src/store.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
agentPluginCredentialSubjectSchema,
|
|
3
|
+
destinationSchema,
|
|
4
|
+
type AgentPluginReadState,
|
|
5
|
+
type AgentPluginState,
|
|
4
6
|
} from "@sentry/junior-plugin-api";
|
|
5
7
|
import { getNextRunAtMs } from "./cadence";
|
|
6
8
|
import type { ScheduledRun, ScheduledTask } from "./types";
|
|
@@ -355,11 +357,42 @@ function canFinishRun(
|
|
|
355
357
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
function parseStoredTask(value: unknown): ScheduledTask | undefined {
|
|
361
|
+
if (!value || typeof value !== "object") {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const record = value as Partial<ScheduledTask>;
|
|
365
|
+
const destination = destinationSchema.safeParse(record.destination);
|
|
366
|
+
if (!destination.success) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
const credentialSubject =
|
|
370
|
+
record.credentialSubject === undefined
|
|
371
|
+
? undefined
|
|
372
|
+
: agentPluginCredentialSubjectSchema.safeParse(record.credentialSubject);
|
|
373
|
+
if (credentialSubject && !credentialSubject.success) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
...(record as ScheduledTask),
|
|
378
|
+
destination: destination.data,
|
|
379
|
+
...(credentialSubject ? { credentialSubject: credentialSubject.data } : {}),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function requireStoredTask(task: ScheduledTask): ScheduledTask {
|
|
384
|
+
const parsed = parseStoredTask(task);
|
|
385
|
+
if (!parsed) {
|
|
386
|
+
throw new Error("Scheduled task routing context is invalid.");
|
|
387
|
+
}
|
|
388
|
+
return parsed;
|
|
389
|
+
}
|
|
390
|
+
|
|
358
391
|
async function getTaskFromState(
|
|
359
392
|
state: AgentPluginReadState,
|
|
360
393
|
taskId: string,
|
|
361
394
|
): Promise<ScheduledTask | undefined> {
|
|
362
|
-
return (await state.get
|
|
395
|
+
return parseStoredTask(await state.get(taskKey(taskId)));
|
|
363
396
|
}
|
|
364
397
|
|
|
365
398
|
async function listTasksFromState(
|
|
@@ -425,10 +458,10 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
425
458
|
}
|
|
426
459
|
|
|
427
460
|
async saveTask(task: ScheduledTask): Promise<void> {
|
|
461
|
+
const next = requireStoredTask(task);
|
|
428
462
|
await withLock(this.state, taskLockKey(task.id), async () => {
|
|
429
|
-
const current =
|
|
430
|
-
|
|
431
|
-
await this.saveTaskRecord(task, current);
|
|
463
|
+
const current = await getTaskFromState(this.state, task.id);
|
|
464
|
+
await this.saveTaskRecord(next, current);
|
|
432
465
|
});
|
|
433
466
|
}
|
|
434
467
|
|
|
@@ -570,8 +603,7 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
570
603
|
}): Promise<void> {
|
|
571
604
|
await withLock(this.state, taskLockKey(args.task.id), async () => {
|
|
572
605
|
const current =
|
|
573
|
-
(await this.state
|
|
574
|
-
undefined;
|
|
606
|
+
(await getTaskFromState(this.state, args.task.id)) ?? undefined;
|
|
575
607
|
if (
|
|
576
608
|
!current ||
|
|
577
609
|
current.status !== "active" ||
|
|
@@ -767,8 +799,7 @@ class PluginStateSchedulerStore implements SchedulerStore {
|
|
|
767
799
|
}): Promise<void> {
|
|
768
800
|
await withLock(this.state, taskLockKey(args.run.taskId), async () => {
|
|
769
801
|
const current =
|
|
770
|
-
(await this.state
|
|
771
|
-
undefined;
|
|
802
|
+
(await getTaskFromState(this.state, args.run.taskId)) ?? undefined;
|
|
772
803
|
if (!current || current.status === "deleted") {
|
|
773
804
|
return;
|
|
774
805
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AgentPluginCredentialSubject,
|
|
3
|
+
Destination,
|
|
4
|
+
} from "@sentry/junior-plugin-api";
|
|
2
5
|
|
|
3
6
|
export type ScheduledTaskStatus = "active" | "paused" | "blocked" | "deleted";
|
|
4
7
|
|
|
@@ -26,19 +29,11 @@ export const SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
|
26
29
|
id: "scheduled-task",
|
|
27
30
|
} satisfies ScheduledTaskExecutionActor);
|
|
28
31
|
|
|
29
|
-
export interface ScheduledTaskDestination {
|
|
30
|
-
platform: "slack";
|
|
31
|
-
teamId: string;
|
|
32
|
-
channelId: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
export interface ScheduledTaskConversationAccess {
|
|
36
33
|
audience: "direct" | "group" | "channel";
|
|
37
34
|
visibility: "private" | "public" | "unknown";
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
export type ScheduledTaskCredentialSubject = AgentPluginCredentialSubject;
|
|
41
|
-
|
|
42
37
|
export type ScheduledCalendarFrequency =
|
|
43
38
|
| "daily"
|
|
44
39
|
| "weekly"
|
|
@@ -76,8 +71,8 @@ export interface ScheduledTask {
|
|
|
76
71
|
createdAtMs: number;
|
|
77
72
|
createdBy: ScheduledTaskPrincipal;
|
|
78
73
|
conversationAccess?: ScheduledTaskConversationAccess;
|
|
79
|
-
credentialSubject?:
|
|
80
|
-
destination:
|
|
74
|
+
credentialSubject?: AgentPluginCredentialSubject;
|
|
75
|
+
destination: Destination;
|
|
81
76
|
executionActor?: ScheduledTaskExecutionActor;
|
|
82
77
|
lastRunAtMs?: number;
|
|
83
78
|
nextRunAtMs?: number;
|