@sentry/junior-scheduler 0.101.0 → 0.102.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/README.md +12 -5
- package/dist/index.js +180 -150
- package/dist/store.d.ts +1 -1
- package/dist/tool-support.d.ts +26 -17
- package/dist/tools/create-task.d.ts +3 -2
- package/dist/tools/delete-task.d.ts +2 -2
- package/dist/tools/list-tasks.d.ts +2 -2
- package/dist/tools/run-task-now.d.ts +2 -2
- package/dist/tools/update-task.d.ts +3 -2
- package/dist/types.d.ts +9 -2
- package/migrations/0001_explicit_credential_mode.sql +4 -0
- package/migrations/meta/0001_snapshot.json +251 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +2 -2
- package/src/plugin.ts +20 -4
- package/src/store.ts +70 -31
- package/src/tool-support.ts +7 -36
- package/src/tools/create-task.ts +10 -8
- package/src/tools/update-task.ts +24 -1
- package/src/types.ts +8 -6
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Junior's durable agent runtime.
|
|
|
6
6
|
## Task Model
|
|
7
7
|
|
|
8
8
|
- A scheduled task records its creator, execution actor, Slack destination,
|
|
9
|
-
prompt text, timezone-aware schedule, recurrence, status, and
|
|
9
|
+
prompt text, timezone-aware schedule, recurrence, credential mode, status, and
|
|
10
|
+
next-run state.
|
|
10
11
|
- SQL schemas and migrations are authoritative for persistence.
|
|
11
12
|
- Schedule parsing normalizes calendar intent before storage; execution does not
|
|
12
13
|
reinterpret the original natural-language request.
|
|
@@ -28,10 +29,16 @@ Junior's durable agent runtime.
|
|
|
28
29
|
## Authority
|
|
29
30
|
|
|
30
31
|
- Creation requires the active Slack actor and destination.
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
32
|
+
- Tasks use system credentials by default. Creator credentials require explicit
|
|
33
|
+
authorization from the verified task creator and work in DMs or channels.
|
|
34
|
+
- Only the creator may enable or re-enable creator credential mode. Any
|
|
35
|
+
conversation manager may disable it, and another user's executable task edit
|
|
36
|
+
clears it; schedule, status, and run-now changes preserve it.
|
|
37
|
+
- Scheduled execution remains a system actor. At dispatch, creator mode derives
|
|
38
|
+
a user credential subject bound to the exact scheduler task; interactive OAuth
|
|
39
|
+
remains disabled.
|
|
40
|
+
- Stored creator attribution grants no broader task-management or execution
|
|
41
|
+
authority.
|
|
35
42
|
- User-visible schedule management remains scoped to the active Slack context.
|
|
36
43
|
|
|
37
44
|
## Operations
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
pluginCredentialSubjectSchema,
|
|
10
10
|
destinationSchema,
|
|
11
|
-
isSlackDestination
|
|
11
|
+
isSlackDestination,
|
|
12
|
+
slackActorSchema
|
|
12
13
|
} from "@sentry/junior-plugin-api";
|
|
13
14
|
import {
|
|
14
15
|
and,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
or,
|
|
22
23
|
sql as sql2
|
|
23
24
|
} from "drizzle-orm";
|
|
24
|
-
import { z } from "zod";
|
|
25
|
+
import { z as z2 } from "zod";
|
|
25
26
|
|
|
26
27
|
// src/cadence.ts
|
|
27
28
|
function parseScheduleTimestamp(value) {
|
|
@@ -416,6 +417,14 @@ var juniorSchedulerRuns = pgTable(
|
|
|
416
417
|
]
|
|
417
418
|
);
|
|
418
419
|
|
|
420
|
+
// src/types.ts
|
|
421
|
+
import { z } from "zod";
|
|
422
|
+
var scheduledTaskCredentialModeSchema = z.enum(["system", "creator"]);
|
|
423
|
+
var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
424
|
+
platform: "system",
|
|
425
|
+
name: "scheduled-task"
|
|
426
|
+
});
|
|
427
|
+
|
|
419
428
|
// src/store.ts
|
|
420
429
|
var SCHEDULER_KEY_PREFIX = "junior:scheduler";
|
|
421
430
|
var SCHEDULER_RECORD_TTL_MS = 5 * 365 * 24 * 60 * 60 * 1e3;
|
|
@@ -425,69 +434,76 @@ var PENDING_CLAIM_STALE_MS = 6e4;
|
|
|
425
434
|
var MISSED_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
426
435
|
var SQL_INCOMPLETE_RUN_STATUSES = ["pending", "running"];
|
|
427
436
|
var slackDestinationSchema = destinationSchema.refine(isSlackDestination);
|
|
428
|
-
var taskPrincipalSchema =
|
|
429
|
-
slackUserId:
|
|
430
|
-
fullName:
|
|
431
|
-
userName:
|
|
437
|
+
var taskPrincipalSchema = z2.object({
|
|
438
|
+
slackUserId: slackActorSchema.shape.userId,
|
|
439
|
+
fullName: z2.string().optional(),
|
|
440
|
+
userName: z2.string().optional()
|
|
432
441
|
}).strict();
|
|
433
|
-
var recurrenceSchema =
|
|
434
|
-
dayOfMonth:
|
|
435
|
-
frequency:
|
|
436
|
-
interval:
|
|
437
|
-
month:
|
|
438
|
-
startDate:
|
|
439
|
-
time:
|
|
440
|
-
hour:
|
|
441
|
-
minute:
|
|
442
|
+
var recurrenceSchema = z2.object({
|
|
443
|
+
dayOfMonth: z2.number().optional(),
|
|
444
|
+
frequency: z2.enum(["daily", "weekly", "monthly", "yearly"]),
|
|
445
|
+
interval: z2.number(),
|
|
446
|
+
month: z2.number().optional(),
|
|
447
|
+
startDate: z2.string(),
|
|
448
|
+
time: z2.object({
|
|
449
|
+
hour: z2.number(),
|
|
450
|
+
minute: z2.number()
|
|
442
451
|
}).strict(),
|
|
443
|
-
weekdays:
|
|
452
|
+
weekdays: z2.array(z2.number()).optional()
|
|
444
453
|
}).strict();
|
|
445
|
-
var taskScheduleSchema =
|
|
446
|
-
description:
|
|
447
|
-
kind:
|
|
454
|
+
var taskScheduleSchema = z2.object({
|
|
455
|
+
description: z2.string(),
|
|
456
|
+
kind: z2.enum(["one_off", "recurring"]),
|
|
448
457
|
recurrence: recurrenceSchema.optional(),
|
|
449
|
-
timezone:
|
|
458
|
+
timezone: z2.string()
|
|
450
459
|
}).strict();
|
|
451
|
-
var taskSpecSchema =
|
|
452
|
-
text:
|
|
460
|
+
var taskSpecSchema = z2.object({
|
|
461
|
+
text: z2.string()
|
|
453
462
|
}).strict();
|
|
454
|
-
var
|
|
455
|
-
id:
|
|
456
|
-
conversationAccess:
|
|
457
|
-
audience:
|
|
458
|
-
visibility:
|
|
463
|
+
var taskRecordFields = {
|
|
464
|
+
id: z2.string(),
|
|
465
|
+
conversationAccess: z2.object({
|
|
466
|
+
audience: z2.enum(["direct", "group", "channel"]),
|
|
467
|
+
visibility: z2.enum(["private", "public", "unknown"])
|
|
459
468
|
}).strict().optional(),
|
|
460
|
-
createdAtMs:
|
|
469
|
+
createdAtMs: z2.number(),
|
|
461
470
|
createdBy: taskPrincipalSchema,
|
|
462
|
-
credentialSubject: pluginCredentialSubjectSchema.optional(),
|
|
463
471
|
destination: slackDestinationSchema,
|
|
464
|
-
executionActor:
|
|
465
|
-
platform:
|
|
466
|
-
name:
|
|
472
|
+
executionActor: z2.object({
|
|
473
|
+
platform: z2.literal("system"),
|
|
474
|
+
name: z2.string()
|
|
467
475
|
}).strict().optional(),
|
|
468
|
-
lastRunAtMs:
|
|
469
|
-
nextRunAtMs:
|
|
470
|
-
originalRequest:
|
|
471
|
-
runNowAtMs:
|
|
476
|
+
lastRunAtMs: z2.number().optional(),
|
|
477
|
+
nextRunAtMs: z2.number().optional(),
|
|
478
|
+
originalRequest: z2.string().optional(),
|
|
479
|
+
runNowAtMs: z2.number().optional(),
|
|
472
480
|
schedule: taskScheduleSchema,
|
|
473
|
-
status:
|
|
474
|
-
statusReason:
|
|
481
|
+
status: z2.enum(["active", "paused", "blocked", "deleted"]),
|
|
482
|
+
statusReason: z2.string().optional(),
|
|
475
483
|
task: taskSpecSchema,
|
|
476
|
-
updatedAtMs:
|
|
477
|
-
version:
|
|
484
|
+
updatedAtMs: z2.number(),
|
|
485
|
+
version: z2.number().optional()
|
|
486
|
+
};
|
|
487
|
+
var taskRecordSchema = z2.object({
|
|
488
|
+
...taskRecordFields,
|
|
489
|
+
credentialMode: scheduledTaskCredentialModeSchema
|
|
490
|
+
}).strict();
|
|
491
|
+
var legacyTaskRecordSchema = z2.object({
|
|
492
|
+
...taskRecordFields,
|
|
493
|
+
credentialSubject: pluginCredentialSubjectSchema.optional()
|
|
478
494
|
}).strict();
|
|
479
|
-
var runRecordSchema =
|
|
480
|
-
id:
|
|
481
|
-
attempt:
|
|
482
|
-
claimedAtMs:
|
|
483
|
-
completedAtMs:
|
|
484
|
-
dispatchId:
|
|
485
|
-
errorMessage:
|
|
486
|
-
idempotencyKey:
|
|
487
|
-
resultMessageTs:
|
|
488
|
-
scheduledForMs:
|
|
489
|
-
startedAtMs:
|
|
490
|
-
status:
|
|
495
|
+
var runRecordSchema = z2.object({
|
|
496
|
+
id: z2.string(),
|
|
497
|
+
attempt: z2.number(),
|
|
498
|
+
claimedAtMs: z2.number(),
|
|
499
|
+
completedAtMs: z2.number().optional(),
|
|
500
|
+
dispatchId: z2.string().optional(),
|
|
501
|
+
errorMessage: z2.string().optional(),
|
|
502
|
+
idempotencyKey: z2.string().optional(),
|
|
503
|
+
resultMessageTs: z2.string().optional(),
|
|
504
|
+
scheduledForMs: z2.number(),
|
|
505
|
+
startedAtMs: z2.number().optional(),
|
|
506
|
+
status: z2.enum([
|
|
491
507
|
"pending",
|
|
492
508
|
"running",
|
|
493
509
|
"completed",
|
|
@@ -495,8 +511,8 @@ var runRecordSchema = z.object({
|
|
|
495
511
|
"blocked",
|
|
496
512
|
"skipped"
|
|
497
513
|
]),
|
|
498
|
-
taskId:
|
|
499
|
-
taskVersion:
|
|
514
|
+
taskId: z2.string(),
|
|
515
|
+
taskVersion: z2.number().optional()
|
|
500
516
|
}).strict();
|
|
501
517
|
function taskKey(taskId) {
|
|
502
518
|
return `${SCHEDULER_KEY_PREFIX}:task:${taskId}`;
|
|
@@ -573,6 +589,8 @@ function normalizedText(value) {
|
|
|
573
589
|
}
|
|
574
590
|
function taskDedupeFingerprint(task) {
|
|
575
591
|
return JSON.stringify({
|
|
592
|
+
credentialMode: task.credentialMode,
|
|
593
|
+
credentialUserId: task.credentialMode === "creator" ? task.createdBy.slackUserId : null,
|
|
576
594
|
destination: task.destination,
|
|
577
595
|
schedule: {
|
|
578
596
|
kind: task.schedule.kind,
|
|
@@ -604,6 +622,21 @@ function parseStoredTask(value) {
|
|
|
604
622
|
const parsed = taskRecordSchema.safeParse(parseJsonRecord(value));
|
|
605
623
|
return parsed.success ? stripLegacyTaskFields(parsed.data) : void 0;
|
|
606
624
|
}
|
|
625
|
+
function parseLegacyStoredTaskForMigration(value) {
|
|
626
|
+
const parsed = legacyTaskRecordSchema.safeParse(parseJsonRecord(value));
|
|
627
|
+
if (!parsed.success) {
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
const {
|
|
631
|
+
credentialSubject: _credentialSubject,
|
|
632
|
+
version: _version,
|
|
633
|
+
...task
|
|
634
|
+
} = parsed.data;
|
|
635
|
+
return {
|
|
636
|
+
...task,
|
|
637
|
+
credentialMode: "system"
|
|
638
|
+
};
|
|
639
|
+
}
|
|
607
640
|
function parseStoredRun(value) {
|
|
608
641
|
const parsed = runRecordSchema.safeParse(parseJsonRecord(value));
|
|
609
642
|
return parsed.success ? stripLegacyRunFields(parsed.data) : void 0;
|
|
@@ -643,9 +676,6 @@ function requireStoredTask(task) {
|
|
|
643
676
|
}
|
|
644
677
|
return parsed;
|
|
645
678
|
}
|
|
646
|
-
async function getTaskFromState(state, taskId) {
|
|
647
|
-
return parseStoredTask(await state.get(taskKey(taskId)));
|
|
648
|
-
}
|
|
649
679
|
async function getRunFromState(state, runId) {
|
|
650
680
|
return parseStoredRun(await state.get(runKey(runId)));
|
|
651
681
|
}
|
|
@@ -1071,7 +1101,8 @@ async function migrateSchedulerStateToSql(args) {
|
|
|
1071
1101
|
let missing = 0;
|
|
1072
1102
|
const migratedTasks = [];
|
|
1073
1103
|
for (const id of ids) {
|
|
1074
|
-
const
|
|
1104
|
+
const rawTask = await args.state.get(taskKey(id));
|
|
1105
|
+
const task = parseStoredTask(rawTask) ?? parseLegacyStoredTaskForMigration(rawTask);
|
|
1075
1106
|
if (!task) {
|
|
1076
1107
|
missing += 1;
|
|
1077
1108
|
continue;
|
|
@@ -1150,62 +1181,55 @@ function scheduledTaskPrincipalLabel(principal) {
|
|
|
1150
1181
|
|
|
1151
1182
|
// src/tools/create-task.ts
|
|
1152
1183
|
import { definePluginTool } from "@sentry/junior-plugin-api";
|
|
1153
|
-
import { z as
|
|
1154
|
-
|
|
1155
|
-
// src/types.ts
|
|
1156
|
-
var SCHEDULED_TASK_SYSTEM_ACTOR = Object.freeze({
|
|
1157
|
-
platform: "system",
|
|
1158
|
-
name: "scheduled-task"
|
|
1159
|
-
});
|
|
1184
|
+
import { z as z4 } from "zod";
|
|
1160
1185
|
|
|
1161
1186
|
// src/tool-support.ts
|
|
1162
1187
|
import { randomUUID } from "crypto";
|
|
1163
1188
|
import {
|
|
1164
1189
|
PluginToolInputError,
|
|
1165
1190
|
pluginToolResultSchema,
|
|
1166
|
-
pluginCredentialSubjectSchema as pluginCredentialSubjectSchema2,
|
|
1167
1191
|
sourceSchema
|
|
1168
1192
|
} from "@sentry/junior-plugin-api";
|
|
1169
|
-
import { z as
|
|
1193
|
+
import { z as z3 } from "zod";
|
|
1170
1194
|
var TASK_ID_PREFIX = "sched";
|
|
1171
1195
|
var MAX_LISTED_TASKS = 50;
|
|
1172
1196
|
var DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
|
|
1173
|
-
var compactTaskResultSchema =
|
|
1174
|
-
id:
|
|
1175
|
-
status:
|
|
1176
|
-
task:
|
|
1177
|
-
schedule:
|
|
1178
|
-
timezone:
|
|
1179
|
-
recurrence:
|
|
1180
|
-
next_run_at:
|
|
1181
|
-
conversation_access:
|
|
1182
|
-
|
|
1183
|
-
last_run_at:
|
|
1184
|
-
run_now_at:
|
|
1197
|
+
var compactTaskResultSchema = z3.object({
|
|
1198
|
+
id: z3.string(),
|
|
1199
|
+
status: z3.enum(["active", "paused", "blocked", "deleted"]),
|
|
1200
|
+
task: z3.string(),
|
|
1201
|
+
schedule: z3.string(),
|
|
1202
|
+
timezone: z3.string(),
|
|
1203
|
+
recurrence: z3.unknown().nullable(),
|
|
1204
|
+
next_run_at: z3.string().nullable(),
|
|
1205
|
+
conversation_access: z3.unknown().nullable(),
|
|
1206
|
+
credential_mode: z3.enum(["system", "creator"]),
|
|
1207
|
+
last_run_at: z3.string().nullable(),
|
|
1208
|
+
run_now_at: z3.string().nullable()
|
|
1185
1209
|
}).strict();
|
|
1186
|
-
var scheduleTaskResultDataSchema =
|
|
1187
|
-
ok:
|
|
1210
|
+
var scheduleTaskResultDataSchema = z3.object({
|
|
1211
|
+
ok: z3.literal(true),
|
|
1188
1212
|
task: compactTaskResultSchema
|
|
1189
1213
|
}).strict();
|
|
1190
1214
|
var scheduleTaskToolResultSchema = pluginToolResultSchema.extend({
|
|
1191
|
-
ok:
|
|
1192
|
-
status:
|
|
1193
|
-
target:
|
|
1215
|
+
ok: z3.literal(true),
|
|
1216
|
+
status: z3.literal("success"),
|
|
1217
|
+
target: z3.string(),
|
|
1194
1218
|
data: scheduleTaskResultDataSchema,
|
|
1195
1219
|
task: compactTaskResultSchema
|
|
1196
1220
|
});
|
|
1197
|
-
var scheduleListResultDataSchema =
|
|
1198
|
-
ok:
|
|
1199
|
-
tasks:
|
|
1200
|
-
truncated:
|
|
1221
|
+
var scheduleListResultDataSchema = z3.object({
|
|
1222
|
+
ok: z3.literal(true),
|
|
1223
|
+
tasks: z3.array(compactTaskResultSchema),
|
|
1224
|
+
truncated: z3.boolean()
|
|
1201
1225
|
}).strict();
|
|
1202
1226
|
var scheduleListToolResultSchema = pluginToolResultSchema.extend({
|
|
1203
|
-
ok:
|
|
1204
|
-
status:
|
|
1205
|
-
target:
|
|
1227
|
+
ok: z3.literal(true),
|
|
1228
|
+
status: z3.literal("success"),
|
|
1229
|
+
target: z3.string(),
|
|
1206
1230
|
data: scheduleListResultDataSchema,
|
|
1207
|
-
tasks:
|
|
1208
|
-
truncated:
|
|
1231
|
+
tasks: z3.array(compactTaskResultSchema),
|
|
1232
|
+
truncated: z3.boolean()
|
|
1209
1233
|
});
|
|
1210
1234
|
function throwToolInputError(error) {
|
|
1211
1235
|
throw new PluginToolInputError(error);
|
|
@@ -1240,8 +1264,8 @@ function requireActiveConversation(context) {
|
|
|
1240
1264
|
channelId: parsed.data.channelId
|
|
1241
1265
|
};
|
|
1242
1266
|
}
|
|
1243
|
-
function requireActor(context) {
|
|
1244
|
-
if (context.actor?.platform !== "slack") {
|
|
1267
|
+
function requireActor(context, destination) {
|
|
1268
|
+
if (context.actor?.platform !== "slack" || context.actor.teamId !== destination.teamId) {
|
|
1245
1269
|
throwToolInputError("No active Slack actor context is available.");
|
|
1246
1270
|
}
|
|
1247
1271
|
const userId = context.actor?.userId?.trim();
|
|
@@ -1269,23 +1293,6 @@ function getConversationAccess(destination) {
|
|
|
1269
1293
|
}
|
|
1270
1294
|
return { audience: "channel", visibility: "unknown" };
|
|
1271
1295
|
}
|
|
1272
|
-
function getCredentialSubject(args) {
|
|
1273
|
-
if (args.access.audience !== "direct" || args.access.visibility !== "private") {
|
|
1274
|
-
return void 0;
|
|
1275
|
-
}
|
|
1276
|
-
if (!args.subject) {
|
|
1277
|
-
return void 0;
|
|
1278
|
-
}
|
|
1279
|
-
const subject = pluginCredentialSubjectSchema2.safeParse(args.subject);
|
|
1280
|
-
if (!subject.success) {
|
|
1281
|
-
throwToolInputError("Active Slack credential subject is invalid.");
|
|
1282
|
-
}
|
|
1283
|
-
return {
|
|
1284
|
-
type: subject.data.type,
|
|
1285
|
-
userId: subject.data.userId,
|
|
1286
|
-
allowedWhen: subject.data.allowedWhen
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
1296
|
function sameDestination(task, destination) {
|
|
1290
1297
|
const taskDestination = task.destination;
|
|
1291
1298
|
return taskDestination.platform === "slack" && taskDestination.teamId === destination.teamId && taskDestination.channelId === destination.channelId;
|
|
@@ -1323,10 +1330,7 @@ function compactTask(task) {
|
|
|
1323
1330
|
} : null,
|
|
1324
1331
|
next_run_at: task.nextRunAtMs ? new Date(task.nextRunAtMs).toISOString() : null,
|
|
1325
1332
|
conversation_access: task.conversationAccess ?? null,
|
|
1326
|
-
|
|
1327
|
-
type: task.credentialSubject.type,
|
|
1328
|
-
allowed_when: task.credentialSubject.allowedWhen
|
|
1329
|
-
} : null,
|
|
1333
|
+
credential_mode: task.credentialMode,
|
|
1330
1334
|
last_run_at: task.lastRunAtMs ? new Date(task.lastRunAtMs).toISOString() : null,
|
|
1331
1335
|
run_now_at: task.runNowAtMs ? new Date(task.runNowAtMs).toISOString() : null
|
|
1332
1336
|
});
|
|
@@ -1451,28 +1455,31 @@ function parseNextRunAtMs(nextRunAtIso) {
|
|
|
1451
1455
|
// src/tools/create-task.ts
|
|
1452
1456
|
function createSlackScheduleCreateTaskTool(context) {
|
|
1453
1457
|
return definePluginTool({
|
|
1454
|
-
description: "Create a one-time or recurring Junior task in the active Slack conversation. For one-time reminders or one-time scheduled work, omit recurrence entirely; never choose a default recurrence. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Do not create scheduled polling tasks to watch provider resources when a subscribable tool result can deliver the requested events. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. When the task, schedule, and
|
|
1458
|
+
description: "Create a one-time or recurring Junior task in the active Slack conversation. For one-time reminders or one-time scheduled work, omit recurrence entirely; never choose a default recurrence. Use only when the user explicitly asks Junior to do work later or on a recurring cadence. Do not create scheduled polling tasks to watch provider resources when a subscribable tool result can deliver the requested events. Only manage tasks for the active Slack DM or channel; never target threads, other channels, or another user's DM. Set credential_mode to creator only when the current user explicitly authorizes future scheduled use of their connected credentials. If connected-credential use is needed but authorization is ambiguous, ask before creating the task. Otherwise omit credential_mode and use system credentials. When the task, schedule, destination, and credential mode are clear, create it without another confirmation.",
|
|
1455
1459
|
executionMode: "sequential",
|
|
1456
|
-
inputSchema:
|
|
1457
|
-
task:
|
|
1458
|
-
schedule:
|
|
1459
|
-
schedule_kind:
|
|
1460
|
+
inputSchema: z4.object({
|
|
1461
|
+
task: z4.string().min(1).max(4e3),
|
|
1462
|
+
schedule: z4.string().min(1).max(300),
|
|
1463
|
+
schedule_kind: z4.enum(["one_off", "recurring"]).describe(
|
|
1460
1464
|
"Required schedule classification. Use one_off for one-time reminders or one-time scheduled work. Use recurring only when the user explicitly asks for a repeating schedule."
|
|
1461
1465
|
),
|
|
1462
|
-
timezone:
|
|
1466
|
+
timezone: z4.string().min(1).max(80).describe(
|
|
1463
1467
|
"IANA timezone, e.g. 'America/Los_Angeles'. Defaults to the channel's configured timezone."
|
|
1464
1468
|
).optional(),
|
|
1465
|
-
next_run_at:
|
|
1469
|
+
next_run_at: z4.string().min(1).describe(
|
|
1466
1470
|
"Exact next run time as an ISO timestamp, computed from the user's requested schedule."
|
|
1467
1471
|
).optional(),
|
|
1468
|
-
recurrence:
|
|
1472
|
+
recurrence: z4.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
|
|
1469
1473
|
"Required when schedule_kind is recurring. Omit when schedule_kind is one_off. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only."
|
|
1474
|
+
).optional(),
|
|
1475
|
+
credential_mode: z4.enum(["system", "creator"]).nullable().describe(
|
|
1476
|
+
"Use creator only when the current user explicitly authorizes future scheduled use of their connected credentials. Omit or use system otherwise."
|
|
1470
1477
|
).optional()
|
|
1471
1478
|
}),
|
|
1472
1479
|
outputSchema: scheduleTaskToolResultSchema,
|
|
1473
1480
|
execute: async (input) => {
|
|
1474
1481
|
const destination = requireActiveConversation(context);
|
|
1475
|
-
const actor = requireActor(context);
|
|
1482
|
+
const actor = requireActor(context, destination);
|
|
1476
1483
|
const nowMs = Date.now();
|
|
1477
1484
|
const timezone = input.timezone ?? getDefaultScheduleTimezone();
|
|
1478
1485
|
validateCreateScheduleKind(input);
|
|
@@ -1490,17 +1497,13 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1490
1497
|
timezone
|
|
1491
1498
|
});
|
|
1492
1499
|
const conversationAccess = getConversationAccess(destination);
|
|
1493
|
-
const credentialSubject = getCredentialSubject({
|
|
1494
|
-
access: conversationAccess,
|
|
1495
|
-
subject: context.credentialSubject
|
|
1496
|
-
});
|
|
1497
1500
|
const task = {
|
|
1498
1501
|
id: buildTaskId(),
|
|
1499
1502
|
createdAtMs: nowMs,
|
|
1500
1503
|
updatedAtMs: nowMs,
|
|
1501
1504
|
createdBy: actor,
|
|
1502
1505
|
conversationAccess,
|
|
1503
|
-
|
|
1506
|
+
credentialMode: input.credential_mode ?? "system",
|
|
1504
1507
|
destination,
|
|
1505
1508
|
executionActor: SCHEDULED_TASK_SYSTEM_ACTOR,
|
|
1506
1509
|
nextRunAtMs,
|
|
@@ -1527,13 +1530,13 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1527
1530
|
|
|
1528
1531
|
// src/tools/delete-task.ts
|
|
1529
1532
|
import { definePluginTool as definePluginTool2 } from "@sentry/junior-plugin-api";
|
|
1530
|
-
import { z as
|
|
1533
|
+
import { z as z5 } from "zod";
|
|
1531
1534
|
function createSlackScheduleDeleteTaskTool(context) {
|
|
1532
1535
|
return definePluginTool2({
|
|
1533
1536
|
description: "Delete one scheduled Junior task from the active Slack conversation. Use only task IDs returned for this conversation. Do not delete schedules from threads, other channels, or another user's DM.",
|
|
1534
1537
|
executionMode: "sequential",
|
|
1535
|
-
inputSchema:
|
|
1536
|
-
task_id:
|
|
1538
|
+
inputSchema: z5.object({
|
|
1539
|
+
task_id: z5.string().min(1).describe(
|
|
1537
1540
|
"ID of the task to delete. Must be from this active Slack conversation."
|
|
1538
1541
|
)
|
|
1539
1542
|
}),
|
|
@@ -1558,12 +1561,12 @@ function createSlackScheduleDeleteTaskTool(context) {
|
|
|
1558
1561
|
|
|
1559
1562
|
// src/tools/list-tasks.ts
|
|
1560
1563
|
import { definePluginTool as definePluginTool3 } from "@sentry/junior-plugin-api";
|
|
1561
|
-
import { z as
|
|
1564
|
+
import { z as z6 } from "zod";
|
|
1562
1565
|
function createSlackScheduleListTasksTool(context) {
|
|
1563
1566
|
return definePluginTool3({
|
|
1564
1567
|
description: "List scheduled Junior tasks for the active Slack conversation. Use when the user asks what is scheduled here, or when task IDs are needed before editing, deleting, or running schedules. Only manages tasks for the active Slack DM or channel.",
|
|
1565
1568
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
1566
|
-
inputSchema:
|
|
1569
|
+
inputSchema: z6.object({}),
|
|
1567
1570
|
outputSchema: scheduleListToolResultSchema,
|
|
1568
1571
|
execute: async () => {
|
|
1569
1572
|
const destination = requireActiveConversation(context);
|
|
@@ -1585,13 +1588,13 @@ function createSlackScheduleListTasksTool(context) {
|
|
|
1585
1588
|
|
|
1586
1589
|
// src/tools/run-task-now.ts
|
|
1587
1590
|
import { definePluginTool as definePluginTool4 } from "@sentry/junior-plugin-api";
|
|
1588
|
-
import { z as
|
|
1591
|
+
import { z as z7 } from "zod";
|
|
1589
1592
|
function createSlackScheduleRunTaskNowTool(context) {
|
|
1590
1593
|
return definePluginTool4({
|
|
1591
1594
|
description: "Queue an existing active scheduled Junior task to run as soon as possible, without changing its cadence. Use when the user asks to run an existing scheduled task now. Use only task IDs returned for this conversation.",
|
|
1592
1595
|
executionMode: "sequential",
|
|
1593
|
-
inputSchema:
|
|
1594
|
-
task_id:
|
|
1596
|
+
inputSchema: z7.object({
|
|
1597
|
+
task_id: z7.string().min(1).describe(
|
|
1595
1598
|
"ID of the active task to run now. Must be from this active Slack conversation."
|
|
1596
1599
|
)
|
|
1597
1600
|
}),
|
|
@@ -1620,24 +1623,27 @@ function createSlackScheduleRunTaskNowTool(context) {
|
|
|
1620
1623
|
|
|
1621
1624
|
// src/tools/update-task.ts
|
|
1622
1625
|
import { definePluginTool as definePluginTool5 } from "@sentry/junior-plugin-api";
|
|
1623
|
-
import { z as
|
|
1626
|
+
import { z as z8 } from "zod";
|
|
1624
1627
|
function createSlackScheduleUpdateTaskTool(context) {
|
|
1625
1628
|
return definePluginTool5({
|
|
1626
|
-
description: "Edit, pause, resume, or
|
|
1629
|
+
description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation. Use only task IDs returned for this conversation. Do not move scheduled tasks across conversations. Set credential_mode to creator only when the task creator explicitly authorizes future scheduled use of their connected credentials. If creator credential use is requested but authorization is ambiguous, ask before enabling it. If another user changes the executable task text, creator credential use is cleared automatically.",
|
|
1627
1630
|
executionMode: "sequential",
|
|
1628
|
-
inputSchema:
|
|
1629
|
-
task_id:
|
|
1631
|
+
inputSchema: z8.object({
|
|
1632
|
+
task_id: z8.string().min(1).describe(
|
|
1630
1633
|
"ID of the task to update. Must be from this active Slack conversation."
|
|
1631
1634
|
),
|
|
1632
|
-
task:
|
|
1633
|
-
schedule:
|
|
1634
|
-
timezone:
|
|
1635
|
-
next_run_at:
|
|
1636
|
-
recurrence:
|
|
1635
|
+
task: z8.string().min(1).max(4e3).optional(),
|
|
1636
|
+
schedule: z8.string().min(1).max(300).optional(),
|
|
1637
|
+
timezone: z8.string().min(1).max(80).optional(),
|
|
1638
|
+
next_run_at: z8.string().min(1).describe("Exact ISO timestamp when changing the next run time.").optional(),
|
|
1639
|
+
recurrence: z8.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
|
|
1637
1640
|
"Provide only for repeating schedules. Omit for one-time requests. Set to null to convert a recurring task to one-time."
|
|
1638
1641
|
).optional(),
|
|
1639
|
-
status:
|
|
1642
|
+
status: z8.enum(["active", "paused", "blocked"]).describe(
|
|
1640
1643
|
"Set to active, paused, or blocked to resume, pause, or block the task."
|
|
1644
|
+
).optional(),
|
|
1645
|
+
credential_mode: z8.enum(["system", "creator"]).nullable().describe(
|
|
1646
|
+
"Set creator only when the current actor is the task creator and explicitly authorizes future scheduled credential use. Set system to disable delegation."
|
|
1641
1647
|
).optional()
|
|
1642
1648
|
}),
|
|
1643
1649
|
outputSchema: scheduleTaskToolResultSchema,
|
|
@@ -1646,6 +1652,13 @@ function createSlackScheduleUpdateTaskTool(context) {
|
|
|
1646
1652
|
context,
|
|
1647
1653
|
taskId: input.task_id
|
|
1648
1654
|
});
|
|
1655
|
+
const actor = requireActor(context, lookup.destination);
|
|
1656
|
+
const isCreator = actor.slackUserId === lookup.createdBy.slackUserId;
|
|
1657
|
+
if (input.credential_mode === "creator" && !isCreator) {
|
|
1658
|
+
throwToolInputError(
|
|
1659
|
+
"Only the scheduled task creator can enable creator credential use."
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1649
1662
|
const timezone = input.timezone ?? lookup.schedule.timezone;
|
|
1650
1663
|
validateRecurringFrequencyLimit(input);
|
|
1651
1664
|
if (!isValidTimeZone(timezone)) {
|
|
@@ -1672,8 +1685,10 @@ function createSlackScheduleUpdateTaskTool(context) {
|
|
|
1672
1685
|
timezone
|
|
1673
1686
|
}) : lookup.schedule.recurrence;
|
|
1674
1687
|
const nextStatus = status ?? lookup.status;
|
|
1688
|
+
const credentialMode = input.task !== void 0 && input.task !== lookup.task.text && !isCreator ? "system" : input.credential_mode ?? lookup.credentialMode;
|
|
1675
1689
|
const next = {
|
|
1676
1690
|
...lookup,
|
|
1691
|
+
credentialMode,
|
|
1677
1692
|
updatedAtMs: Date.now(),
|
|
1678
1693
|
nextRunAtMs,
|
|
1679
1694
|
runNowAtMs: nextStatus === "active" ? lookup.runNowAtMs : void 0,
|
|
@@ -1752,7 +1767,6 @@ function shouldSkipRun(task, run) {
|
|
|
1752
1767
|
}
|
|
1753
1768
|
function createSchedulerToolContext(ctx) {
|
|
1754
1769
|
return {
|
|
1755
|
-
credentialSubject: ctx.slack?.credentialSubject,
|
|
1756
1770
|
source: ctx.source.platform === "slack" ? ctx.source : void 0,
|
|
1757
1771
|
actor: ctx.actor?.platform === "slack" ? ctx.actor : void 0,
|
|
1758
1772
|
store: schedulerStore2(ctx),
|
|
@@ -2007,6 +2021,16 @@ function createSchedulerPlugin() {
|
|
|
2007
2021
|
},
|
|
2008
2022
|
packageName: "@sentry/junior-scheduler",
|
|
2009
2023
|
hooks: {
|
|
2024
|
+
systemPrompt(ctx) {
|
|
2025
|
+
if (ctx.platform !== "slack") {
|
|
2026
|
+
return [];
|
|
2027
|
+
}
|
|
2028
|
+
return [
|
|
2029
|
+
{
|
|
2030
|
+
text: "Scheduled tasks use system credentials by default. Use the task creator's connected credentials only when they explicitly authorize future scheduled use. Requests such as 'use my account if needed' are ambiguous, not authorization; ask before creating or enabling creator credentials for the task."
|
|
2031
|
+
}
|
|
2032
|
+
];
|
|
2033
|
+
},
|
|
2010
2034
|
tools(ctx) {
|
|
2011
2035
|
if (ctx.source.platform !== "slack" || ctx.actor?.platform !== "slack") {
|
|
2012
2036
|
return {};
|
|
@@ -2089,9 +2113,15 @@ function createSchedulerPlugin() {
|
|
|
2089
2113
|
}
|
|
2090
2114
|
let dispatch;
|
|
2091
2115
|
try {
|
|
2116
|
+
const credentialSubject = task.credentialMode === "creator" ? {
|
|
2117
|
+
type: "user",
|
|
2118
|
+
userId: task.createdBy.slackUserId,
|
|
2119
|
+
allowedWhen: "scheduled-task",
|
|
2120
|
+
taskId: task.id
|
|
2121
|
+
} : void 0;
|
|
2092
2122
|
dispatch = await ctx.agent.dispatch({
|
|
2093
2123
|
idempotencyKey: run.id,
|
|
2094
|
-
...
|
|
2124
|
+
...credentialSubject ? { credentialSubject } : {},
|
|
2095
2125
|
destination: task.destination,
|
|
2096
2126
|
input: task.task.text,
|
|
2097
2127
|
metadata: dispatchMetadata,
|
package/dist/store.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type PluginReadState, type PluginState } from "@sentry/junior-plugin-ap
|
|
|
2
2
|
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
3
3
|
import type { PgQueryResultHKT } from "drizzle-orm/pg-core/session";
|
|
4
4
|
import * as schedulerSqlSchema from "./db/schema";
|
|
5
|
-
import type
|
|
5
|
+
import { type ScheduledRun, type ScheduledTask } from "./types";
|
|
6
6
|
export type SchedulerDb = PgDatabase<PgQueryResultHKT, typeof schedulerSqlSchema>;
|
|
7
7
|
export interface SchedulerStore {
|
|
8
8
|
claimDueRun(args: {
|