@sentry/junior-scheduler 0.100.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 ADDED
@@ -0,0 +1,52 @@
1
+ # @sentry/junior-scheduler
2
+
3
+ The scheduler plugin stores user-created schedules and dispatches due work into
4
+ Junior's durable agent runtime.
5
+
6
+ ## Task Model
7
+
8
+ - A scheduled task records its creator, execution actor, Slack destination,
9
+ prompt text, timezone-aware schedule, recurrence, credential mode, status, and
10
+ next-run state.
11
+ - SQL schemas and migrations are authoritative for persistence.
12
+ - Schedule parsing normalizes calendar intent before storage; execution does not
13
+ reinterpret the original natural-language request.
14
+ - Updates and deletion invalidate obsolete pending run times.
15
+
16
+ ## Dispatch
17
+
18
+ - Heartbeat claims a bounded number of due runs.
19
+ - Claiming and completion transitions are atomic and safe to retry.
20
+ - Each run dispatches with explicit source, destination, creator attribution,
21
+ execution actor, metadata, and idempotency identity.
22
+ - A deleted, paused, or rescheduled task is skipped when its claimed run no
23
+ longer matches current task state.
24
+ - Dispatch completion, failure, or blocking updates both the run and the task's
25
+ next-run state.
26
+ - Missed recurring work advances according to the stored calendar rather than
27
+ creating an unbounded catch-up burst.
28
+
29
+ ## Authority
30
+
31
+ - Creation requires the active Slack actor and destination.
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.
42
+ - User-visible schedule management remains scoped to the active Slack context.
43
+
44
+ ## Operations
45
+
46
+ The plugin exposes create, update, delete, list, and run-now tools plus bounded
47
+ operational reporting. Generate schema changes with
48
+ `pnpm --filter @sentry/junior-scheduler db:generate`.
49
+
50
+ Follow `../../policies/serverless-background-work.md`,
51
+ `../../policies/context-bound-systems.md`, and
52
+ `../junior-plugin-api/README.md`.
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 = z.object({
429
- slackUserId: z.string(),
430
- fullName: z.string().optional(),
431
- userName: z.string().optional()
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 = z.object({
434
- dayOfMonth: z.number().optional(),
435
- frequency: z.enum(["daily", "weekly", "monthly", "yearly"]),
436
- interval: z.number(),
437
- month: z.number().optional(),
438
- startDate: z.string(),
439
- time: z.object({
440
- hour: z.number(),
441
- minute: z.number()
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: z.array(z.number()).optional()
452
+ weekdays: z2.array(z2.number()).optional()
444
453
  }).strict();
445
- var taskScheduleSchema = z.object({
446
- description: z.string(),
447
- kind: z.enum(["one_off", "recurring"]),
454
+ var taskScheduleSchema = z2.object({
455
+ description: z2.string(),
456
+ kind: z2.enum(["one_off", "recurring"]),
448
457
  recurrence: recurrenceSchema.optional(),
449
- timezone: z.string()
458
+ timezone: z2.string()
450
459
  }).strict();
451
- var taskSpecSchema = z.object({
452
- text: z.string()
460
+ var taskSpecSchema = z2.object({
461
+ text: z2.string()
453
462
  }).strict();
454
- var taskRecordSchema = z.object({
455
- id: z.string(),
456
- conversationAccess: z.object({
457
- audience: z.enum(["direct", "group", "channel"]),
458
- visibility: z.enum(["private", "public", "unknown"])
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: z.number(),
469
+ createdAtMs: z2.number(),
461
470
  createdBy: taskPrincipalSchema,
462
- credentialSubject: pluginCredentialSubjectSchema.optional(),
463
471
  destination: slackDestinationSchema,
464
- executionActor: z.object({
465
- platform: z.literal("system"),
466
- name: z.string()
472
+ executionActor: z2.object({
473
+ platform: z2.literal("system"),
474
+ name: z2.string()
467
475
  }).strict().optional(),
468
- lastRunAtMs: z.number().optional(),
469
- nextRunAtMs: z.number().optional(),
470
- originalRequest: z.string().optional(),
471
- runNowAtMs: z.number().optional(),
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: z.enum(["active", "paused", "blocked", "deleted"]),
474
- statusReason: z.string().optional(),
481
+ status: z2.enum(["active", "paused", "blocked", "deleted"]),
482
+ statusReason: z2.string().optional(),
475
483
  task: taskSpecSchema,
476
- updatedAtMs: z.number(),
477
- version: z.number().optional()
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 = z.object({
480
- id: z.string(),
481
- attempt: z.number(),
482
- claimedAtMs: z.number(),
483
- completedAtMs: z.number().optional(),
484
- dispatchId: z.string().optional(),
485
- errorMessage: z.string().optional(),
486
- idempotencyKey: z.string().optional(),
487
- resultMessageTs: z.string().optional(),
488
- scheduledForMs: z.number(),
489
- startedAtMs: z.number().optional(),
490
- status: z.enum([
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: z.string(),
499
- taskVersion: z.number().optional()
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 task = await getTaskFromState(args.state, id);
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 z3 } from "zod";
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 z2 } from "zod";
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 = z2.object({
1174
- id: z2.string(),
1175
- status: z2.enum(["active", "paused", "blocked", "deleted"]),
1176
- task: z2.string(),
1177
- schedule: z2.string(),
1178
- timezone: z2.string(),
1179
- recurrence: z2.unknown().nullable(),
1180
- next_run_at: z2.string().nullable(),
1181
- conversation_access: z2.unknown().nullable(),
1182
- credential_subject: z2.unknown().nullable(),
1183
- last_run_at: z2.string().nullable(),
1184
- run_now_at: z2.string().nullable()
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 = z2.object({
1187
- ok: z2.literal(true),
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: z2.literal(true),
1192
- status: z2.literal("success"),
1193
- target: z2.string(),
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 = z2.object({
1198
- ok: z2.literal(true),
1199
- tasks: z2.array(compactTaskResultSchema),
1200
- truncated: z2.boolean()
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: z2.literal(true),
1204
- status: z2.literal("success"),
1205
- target: z2.string(),
1227
+ ok: z3.literal(true),
1228
+ status: z3.literal("success"),
1229
+ target: z3.string(),
1206
1230
  data: scheduleListResultDataSchema,
1207
- tasks: z2.array(compactTaskResultSchema),
1208
- truncated: z2.boolean()
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
- credential_subject: task.credentialSubject ? {
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 destination are clear, create it without asking for confirmation; ask only when one of those is ambiguous.",
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: z3.object({
1457
- task: z3.string().min(1).max(4e3),
1458
- schedule: z3.string().min(1).max(300),
1459
- schedule_kind: z3.enum(["one_off", "recurring"]).describe(
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: z3.string().min(1).max(80).describe(
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: z3.string().min(1).describe(
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: z3.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
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
- ...credentialSubject ? { credentialSubject } : {},
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 z4 } from "zod";
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: z4.object({
1536
- task_id: z4.string().min(1).describe(
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 z5 } from "zod";
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: z5.object({}),
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 z6 } from "zod";
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: z6.object({
1594
- task_id: z6.string().min(1).describe(
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 z7 } from "zod";
1626
+ import { z as z8 } from "zod";
1624
1627
  function createSlackScheduleUpdateTaskTool(context) {
1625
1628
  return definePluginTool5({
1626
- description: "Edit, pause, resume, or reschedule 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.",
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: z7.object({
1629
- task_id: z7.string().min(1).describe(
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: z7.string().min(1).max(4e3).optional(),
1633
- schedule: z7.string().min(1).max(300).optional(),
1634
- timezone: z7.string().min(1).max(80).optional(),
1635
- next_run_at: z7.string().min(1).describe("Exact ISO timestamp when changing the next run time.").optional(),
1636
- recurrence: z7.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
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: z7.enum(["active", "paused", "blocked"]).describe(
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
- ...task.credentialSubject ? { credentialSubject: task.credentialSubject } : {},
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 { ScheduledRun, ScheduledTask } from "./types";
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: {