@sentry/junior-plugin-api 0.122.1 → 0.123.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 CHANGED
@@ -5,8 +5,10 @@ exported TypeScript types and runtime validators are authoritative.
5
5
 
6
6
  ## Registration
7
7
 
8
- Use `defineJuniorPlugin({ manifest, hooks, tasks, cli, model })`. A plugin name
9
- is a lowercase identifier and is unique within the enabled app plugin set.
8
+ Use
9
+ `defineJuniorPlugin({ manifest, hooks, tasks, cli, model, conversationEvents })`.
10
+ A plugin name is a lowercase identifier and is unique within the enabled app
11
+ plugin set.
10
12
 
11
13
  Packages with JavaScript registration export one callable factory named
12
14
  `<domain>Plugin`, such as `githubPlugin(options?)`. Do not use a public
@@ -68,6 +70,11 @@ routing, response validation, rendering, confirmation, and query state.
68
70
  repeatedly.
69
71
  - Background tasks are registered by name, receive validated parameters, and
70
72
  execute through the host queue/callback lifecycle.
73
+ - Conversation-bound background tasks may emit registered structured events
74
+ through `ctx.events`. Define each version with `defineConversationEvent()`;
75
+ the host supplies the plugin namespace, conversation, turn, ordering, and
76
+ timestamps. Event definitions return bounded transcript presentation data,
77
+ while Junior owns browser rendering.
71
78
  - `ctx.agent.dispatch` creates durable agent work with an explicit actor,
72
79
  destination, source, metadata, and idempotency identity.
73
80
  - Delegated credential subjects declare the narrow action that authorized them.
@@ -76,6 +83,15 @@ routing, response validation, rendering, confirmation, and query state.
76
83
  - Completed dispatch and task projections are durable plugin inputs, not an
77
84
  invitation to inspect unrestricted conversation state.
78
85
 
86
+ ## Conversation Events
87
+
88
+ Register plugin-owned event definitions through `conversationEvents`. A
89
+ definition owns one local name, version, content schema, and `renderEvent()`
90
+ projection. The active plugin context supplies the namespace, so plugins cannot
91
+ emit native events or impersonate another plugin. Stored events remain durable
92
+ when a plugin is removed, but normal transcript projection skips definitions
93
+ that are not currently registered.
94
+
79
95
  ## Database
80
96
 
81
97
  - Packaged migrations create plugin-owned tables through the host migration
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ export declare const conversationEventIconSchema: z.ZodEnum<{
3
+ check: "check";
4
+ key: "key";
5
+ warning: "warning";
6
+ link: "link";
7
+ activity: "activity";
8
+ brain: "brain";
9
+ calendar: "calendar";
10
+ database: "database";
11
+ info: "info";
12
+ sparkles: "sparkles";
13
+ }>;
14
+ /** Safe, core-rendered presentation for one plugin conversation event. */
15
+ export declare const conversationEventPresentationSchema: z.ZodObject<{
16
+ details: z.ZodOptional<z.ZodArray<z.ZodObject<{
17
+ description: z.ZodOptional<z.ZodString>;
18
+ metadata: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ title: z.ZodString;
20
+ }, z.core.$strict>>>;
21
+ icon: z.ZodOptional<z.ZodEnum<{
22
+ check: "check";
23
+ key: "key";
24
+ warning: "warning";
25
+ link: "link";
26
+ activity: "activity";
27
+ brain: "brain";
28
+ calendar: "calendar";
29
+ database: "database";
30
+ info: "info";
31
+ sparkles: "sparkles";
32
+ }>>;
33
+ preview: z.ZodOptional<z.ZodString>;
34
+ title: z.ZodString;
35
+ }, z.core.$strict>;
36
+ export type ConversationEventPresentation = z.output<typeof conversationEventPresentationSchema>;
37
+ /** One validated plugin event value waiting for conversation-bound emission. */
38
+ export interface PluginConversationEventValue {
39
+ readonly data: Record<string, unknown>;
40
+ readonly definition: PluginConversationEventDefinition;
41
+ }
42
+ /** Registered schema and transcript presentation for one plugin event version. */
43
+ export interface PluginConversationEventDefinition {
44
+ readonly eventName: string;
45
+ readonly version: number;
46
+ parse(data: unknown): Record<string, unknown>;
47
+ renderEvent(data: Record<string, unknown>): ConversationEventPresentation;
48
+ }
49
+ /** Typed factory returned while authoring one plugin conversation event. */
50
+ export interface DefinedConversationEvent<TInput> extends PluginConversationEventDefinition {
51
+ (data: TInput): PluginConversationEventValue;
52
+ }
53
+ /** Define one typed, versioned plugin-owned conversation event. */
54
+ export declare function defineConversationEvent<TSchema extends z.ZodType<Record<string, unknown>>>(definition: {
55
+ name: string;
56
+ version: number;
57
+ schema: TSchema;
58
+ renderEvent(event: z.output<TSchema>): z.input<typeof conversationEventPresentationSchema>;
59
+ }): DefinedConversationEvent<z.input<TSchema>>;
60
+ /** Conversation-bound event writer supplied by Junior core. */
61
+ export interface PluginConversationEvents {
62
+ emit(event: PluginConversationEventValue): Promise<void>;
63
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./annotations";
2
+ export * from "./conversation-events";
2
3
  export * from "./schemas";
3
4
  export * from "./context";
4
5
  export * from "./state";
package/dist/index.js CHANGED
@@ -20,32 +20,84 @@ var conversationAnnotationInputSchema = z.discriminatedUnion("kind", [
20
20
  resourceLinkAnnotationSchema
21
21
  ]);
22
22
 
23
- // src/schemas.ts
23
+ // src/conversation-events.ts
24
24
  import { z as z2 } from "zod";
25
- var slackTeamIdSchema = z2.string().regex(/^T[A-Z0-9]+$/);
26
- var slackConversationIdSchema = z2.string().regex(/^(C|G|D)[A-Z0-9]+$/);
27
- var localConversationIdSchema = z2.string().regex(/^local:[a-z0-9_-]+:[a-z0-9][a-z0-9_-]*$/);
28
- var exactActorUserIdSchema = z2.string().min(1).refine(
25
+ var conversationEventNameSchema = z2.string().trim().min(1).max(64).regex(/^[a-z][a-z0-9_]*$/);
26
+ var conversationEventIconSchema = z2.enum([
27
+ "activity",
28
+ "brain",
29
+ "calendar",
30
+ "check",
31
+ "database",
32
+ "info",
33
+ "key",
34
+ "link",
35
+ "sparkles",
36
+ "warning"
37
+ ]);
38
+ var conversationEventDetailSchema = z2.object({
39
+ description: z2.string().trim().min(1).max(2e3).optional(),
40
+ metadata: z2.array(z2.string().trim().min(1).max(120)).max(8).optional(),
41
+ title: z2.string().trim().min(1).max(4e3)
42
+ }).strict();
43
+ var conversationEventPresentationSchema = z2.object({
44
+ details: z2.array(conversationEventDetailSchema).max(100).optional(),
45
+ icon: conversationEventIconSchema.optional(),
46
+ preview: z2.string().trim().min(1).max(500).optional(),
47
+ title: z2.string().trim().min(1).max(120)
48
+ }).strict();
49
+ function defineConversationEvent(definition) {
50
+ const identity = z2.object({
51
+ name: conversationEventNameSchema,
52
+ version: z2.number().int().positive()
53
+ }).strict().parse({ name: definition.name, version: definition.version });
54
+ let eventDefinition;
55
+ const createEvent = (data) => ({
56
+ data: definition.schema.parse(data),
57
+ definition: eventDefinition
58
+ });
59
+ eventDefinition = Object.assign(createEvent, {
60
+ eventName: identity.name,
61
+ version: identity.version,
62
+ parse(data) {
63
+ return definition.schema.parse(data);
64
+ },
65
+ renderEvent(data) {
66
+ const parsed = definition.schema.parse(data);
67
+ return conversationEventPresentationSchema.parse(
68
+ definition.renderEvent(parsed)
69
+ );
70
+ }
71
+ });
72
+ return eventDefinition;
73
+ }
74
+
75
+ // src/schemas.ts
76
+ import { z as z3 } from "zod";
77
+ var slackTeamIdSchema = z3.string().regex(/^T[A-Z0-9]+$/);
78
+ var slackConversationIdSchema = z3.string().regex(/^(C|G|D)[A-Z0-9]+$/);
79
+ var localConversationIdSchema = z3.string().regex(/^local:[a-z0-9_-]+:[a-z0-9][a-z0-9_-]*$/);
80
+ var exactActorUserIdSchema = z3.string().min(1).refine(
29
81
  (value) => value === value.trim() && value.toLowerCase() !== "unknown"
30
82
  );
31
- var nonBlankStringSchema = z2.string().refine((value) => value.trim().length > 0);
83
+ var nonBlankStringSchema = z3.string().refine((value) => value.trim().length > 0);
32
84
  var exactNonBlankStringSchema = nonBlankStringSchema.refine(
33
85
  (value) => value === value.trim()
34
86
  );
35
- var platformSchema = z2.enum(["slack", "local"]);
36
- var sourceTypeSchema = z2.enum(["pub", "priv"]);
37
- var destinationVisibilitySchema = z2.enum(["public", "private"]);
38
- var slackAddressSchema = z2.object({
39
- platform: z2.literal("slack"),
87
+ var platformSchema = z3.enum(["slack", "local"]);
88
+ var sourceTypeSchema = z3.enum(["pub", "priv"]);
89
+ var destinationVisibilitySchema = z3.enum(["public", "private"]);
90
+ var slackAddressSchema = z3.object({
91
+ platform: z3.literal("slack"),
40
92
  teamId: slackTeamIdSchema,
41
93
  channelId: slackConversationIdSchema
42
94
  }).strict();
43
95
  var slackDestinationSchema = slackAddressSchema;
44
- var localDestinationSchema = z2.object({
45
- platform: z2.literal("local"),
96
+ var localDestinationSchema = z3.object({
97
+ platform: z3.literal("local"),
46
98
  conversationId: localConversationIdSchema
47
99
  }).strict();
48
- var destinationSchema = z2.discriminatedUnion("platform", [
100
+ var destinationSchema = z3.discriminatedUnion("platform", [
49
101
  slackDestinationSchema,
50
102
  localDestinationSchema
51
103
  ]);
@@ -54,27 +106,27 @@ var slackSourceSchema = slackAddressSchema.extend({
54
106
  messageTs: nonBlankStringSchema.optional(),
55
107
  threadTs: nonBlankStringSchema.optional()
56
108
  }).strict();
57
- var localSourceSchema = z2.object({
58
- platform: z2.literal("local"),
59
- type: z2.literal("priv"),
109
+ var localSourceSchema = z3.object({
110
+ platform: z3.literal("local"),
111
+ type: z3.literal("priv"),
60
112
  conversationId: localConversationIdSchema
61
113
  }).strict();
62
- var sourceSchema = z2.discriminatedUnion("platform", [
114
+ var sourceSchema = z3.discriminatedUnion("platform", [
63
115
  slackSourceSchema,
64
116
  localSourceSchema
65
117
  ]);
66
- var pluginCredentialSubjectSchema = z2.discriminatedUnion(
118
+ var pluginCredentialSubjectSchema = z3.discriminatedUnion(
67
119
  "allowedWhen",
68
120
  [
69
- z2.object({
70
- type: z2.literal("user"),
121
+ z3.object({
122
+ type: z3.literal("user"),
71
123
  userId: exactActorUserIdSchema,
72
- allowedWhen: z2.literal("private-direct-conversation")
124
+ allowedWhen: z3.literal("private-direct-conversation")
73
125
  }).strict(),
74
- z2.object({
75
- type: z2.literal("user"),
126
+ z3.object({
127
+ type: z3.literal("user"),
76
128
  userId: exactActorUserIdSchema,
77
- allowedWhen: z2.literal("scheduled-task"),
129
+ allowedWhen: z3.literal("scheduled-task"),
78
130
  taskId: exactNonBlankStringSchema
79
131
  }).strict()
80
132
  ]
@@ -85,29 +137,29 @@ var actorProfileSchema = {
85
137
  userId: exactActorUserIdSchema,
86
138
  userName: nonBlankStringSchema.optional()
87
139
  };
88
- var slackActorSchema = z2.object({
140
+ var slackActorSchema = z3.object({
89
141
  ...actorProfileSchema,
90
- platform: z2.literal("slack"),
142
+ platform: z3.literal("slack"),
91
143
  teamId: slackTeamIdSchema
92
144
  }).strict();
93
- var localActorSchema = z2.object({
145
+ var localActorSchema = z3.object({
94
146
  ...actorProfileSchema,
95
- platform: z2.literal("local")
147
+ platform: z3.literal("local")
96
148
  }).strict();
97
- var systemActorSchema = z2.object({
98
- platform: z2.literal("system"),
149
+ var systemActorSchema = z3.object({
150
+ platform: z3.literal("system"),
99
151
  name: exactActorUserIdSchema
100
152
  }).strict();
101
- var actorSchema = z2.discriminatedUnion("platform", [
153
+ var actorSchema = z3.discriminatedUnion("platform", [
102
154
  slackActorSchema,
103
155
  localActorSchema,
104
156
  systemActorSchema
105
157
  ]);
106
- var dispatchMetadataSchema = z2.record(z2.string(), z2.string()).superRefine((metadata, ctx) => {
158
+ var dispatchMetadataSchema = z3.record(z3.string(), z3.string()).superRefine((metadata, ctx) => {
107
159
  const entries = Object.entries(metadata);
108
160
  if (entries.length > 20) {
109
161
  ctx.addIssue({
110
- code: z2.ZodIssueCode.custom,
162
+ code: z3.ZodIssueCode.custom,
111
163
  message: "Dispatch metadata has too many keys"
112
164
  });
113
165
  return;
@@ -115,7 +167,7 @@ var dispatchMetadataSchema = z2.record(z2.string(), z2.string()).superRefine((me
115
167
  for (const [key, value] of entries) {
116
168
  if (!key.trim()) {
117
169
  ctx.addIssue({
118
- code: z2.ZodIssueCode.custom,
170
+ code: z3.ZodIssueCode.custom,
119
171
  message: "Dispatch metadata values must be strings",
120
172
  path: [key]
121
173
  });
@@ -123,40 +175,40 @@ var dispatchMetadataSchema = z2.record(z2.string(), z2.string()).superRefine((me
123
175
  }
124
176
  if (key.length > 128) {
125
177
  ctx.addIssue({
126
- code: z2.ZodIssueCode.custom,
178
+ code: z3.ZodIssueCode.custom,
127
179
  message: "Dispatch metadata key exceeds the maximum length",
128
180
  path: [key]
129
181
  });
130
182
  }
131
183
  if (/[\r\n]/.test(key)) {
132
184
  ctx.addIssue({
133
- code: z2.ZodIssueCode.custom,
185
+ code: z3.ZodIssueCode.custom,
134
186
  message: "Dispatch metadata keys must be single-line strings",
135
187
  path: [key]
136
188
  });
137
189
  }
138
190
  if (/[\r\n]/.test(value)) {
139
191
  ctx.addIssue({
140
- code: z2.ZodIssueCode.custom,
192
+ code: z3.ZodIssueCode.custom,
141
193
  message: "Dispatch metadata values must be single-line strings",
142
194
  path: [key]
143
195
  });
144
196
  }
145
197
  if (value.length > 512) {
146
198
  ctx.addIssue({
147
- code: z2.ZodIssueCode.custom,
199
+ code: z3.ZodIssueCode.custom,
148
200
  message: "Dispatch metadata value exceeds the maximum length",
149
201
  path: [key]
150
202
  });
151
203
  }
152
204
  }
153
205
  });
154
- var dispatchOptionsSchema = z2.object({
155
- idempotencyKey: nonBlankStringSchema.pipe(z2.string().max(512)),
206
+ var dispatchOptionsSchema = z3.object({
207
+ idempotencyKey: nonBlankStringSchema.pipe(z3.string().max(512)),
156
208
  credentialSubject: pluginCredentialSubjectSchema.optional(),
157
209
  destination: slackDestinationSchema,
158
210
  destinationVisibility: destinationVisibilitySchema,
159
- input: nonBlankStringSchema.pipe(z2.string().max(32e3)),
211
+ input: nonBlankStringSchema.pipe(z3.string().max(32e3)),
160
212
  metadata: dispatchMetadataSchema.optional(),
161
213
  source: sourceSchema
162
214
  }).strict();
@@ -197,15 +249,15 @@ function isSlackDestination(destination) {
197
249
  }
198
250
 
199
251
  // src/prompt.ts
200
- import { z as z3 } from "zod";
201
- var promptContextKindSchema = z3.string().trim().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/);
202
- var promptMessageSchema = z3.object({
203
- text: z3.string().trim().min(1).max(8e3)
252
+ import { z as z4 } from "zod";
253
+ var promptContextKindSchema = z4.string().trim().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/);
254
+ var promptMessageSchema = z4.object({
255
+ text: z4.string().trim().min(1).max(8e3)
204
256
  }).strict();
205
- var promptContextSchema = z3.object({
257
+ var promptContextSchema = z4.object({
206
258
  kind: promptContextKindSchema,
207
- version: z3.number().int().positive(),
208
- content: z3.record(z3.string(), z3.unknown())
259
+ version: z4.number().int().positive(),
260
+ content: z4.record(z4.string(), z4.unknown())
209
261
  }).strict();
210
262
  function definePromptContext(definition) {
211
263
  const identity = promptContextSchema.pick({ kind: true, version: true }).parse({ kind: definition.kind, version: definition.version });
@@ -219,50 +271,50 @@ function definePromptContext(definition) {
219
271
  }
220
272
 
221
273
  // src/resource-events.ts
222
- import { z as z4 } from "zod";
223
- var subscribableResourceSchema = z4.object({
224
- label: z4.string().min(1),
225
- provider: z4.string().min(1),
226
- resourceRef: z4.string().min(1),
227
- suggestedEvents: z4.array(z4.string().min(1)).optional(),
228
- supportedEvents: z4.array(z4.string().min(1)),
229
- type: z4.string().min(1)
230
- }).strict();
231
- var resourceEventSchema = z4.object({
232
- eventKey: z4.string().min(1),
233
- eventType: z4.string().min(1),
234
- occurredAtMs: z4.number().finite(),
235
- provider: z4.string().min(1),
236
- resourceRef: z4.string().min(1),
237
- terminal: z4.boolean().optional(),
238
- trustedSummary: z4.string().min(1),
239
- untrustedText: z4.string().optional()
274
+ import { z as z5 } from "zod";
275
+ var subscribableResourceSchema = z5.object({
276
+ label: z5.string().min(1),
277
+ provider: z5.string().min(1),
278
+ resourceRef: z5.string().min(1),
279
+ suggestedEvents: z5.array(z5.string().min(1)).optional(),
280
+ supportedEvents: z5.array(z5.string().min(1)),
281
+ type: z5.string().min(1)
282
+ }).strict();
283
+ var resourceEventSchema = z5.object({
284
+ eventKey: z5.string().min(1),
285
+ eventType: z5.string().min(1),
286
+ occurredAtMs: z5.number().finite(),
287
+ provider: z5.string().min(1),
288
+ resourceRef: z5.string().min(1),
289
+ terminal: z5.boolean().optional(),
290
+ trustedSummary: z5.string().min(1),
291
+ untrustedText: z5.string().optional()
240
292
  }).strict();
241
293
 
242
294
  // src/tasks.ts
243
- import { z as z5 } from "zod";
244
- var pluginRunTranscriptProvenanceSchema = z5.object({
245
- authority: z5.enum(["instruction", "context"]),
295
+ import { z as z6 } from "zod";
296
+ var pluginRunTranscriptProvenanceSchema = z6.object({
297
+ authority: z6.enum(["instruction", "context"]),
246
298
  actor: actorSchema.optional()
247
299
  }).strict();
248
- var pluginRunTranscriptEntrySchema = z5.discriminatedUnion("type", [
249
- z5.object({
250
- type: z5.literal("message"),
251
- role: z5.enum(["user", "assistant"]),
252
- text: z5.string().min(1),
300
+ var pluginRunTranscriptEntrySchema = z6.discriminatedUnion("type", [
301
+ z6.object({
302
+ type: z6.literal("message"),
303
+ role: z6.enum(["user", "assistant"]),
304
+ text: z6.string().min(1),
253
305
  provenance: pluginRunTranscriptProvenanceSchema.optional(),
254
- isRunActor: z5.boolean().optional()
306
+ isRunActor: z6.boolean().optional()
255
307
  }).strict(),
256
- z5.object({
257
- type: z5.literal("toolResult"),
258
- toolName: z5.string().min(1),
259
- isError: z5.boolean(),
260
- text: z5.string().min(1).optional()
308
+ z6.object({
309
+ type: z6.literal("toolResult"),
310
+ toolName: z6.string().min(1),
311
+ isError: z6.boolean(),
312
+ text: z6.string().min(1).optional()
261
313
  }).strict()
262
314
  ]);
263
- var pluginRunContextSchema = z5.object({
264
- completedAtMs: z5.number().finite(),
265
- conversationId: z5.string().min(1),
315
+ var pluginRunContextSchema = z6.object({
316
+ completedAtMs: z6.number().finite(),
317
+ conversationId: z6.string().min(1),
266
318
  destination: destinationSchema,
267
319
  /**
268
320
  * All distinct actors annotated on this run's committed instruction-authority
@@ -272,52 +324,63 @@ var pluginRunContextSchema = z5.object({
272
324
  * exceed the actors visible in the transcript slice. Usually `[run.actor]`;
273
325
  * possibly empty for system runs with no human instructions.
274
326
  */
275
- actors: z5.array(actorSchema),
327
+ actors: z6.array(actorSchema),
276
328
  /**
277
329
  * The single actor this run executes as. Absent only for actor-less legacy
278
330
  * system records, so authority-sensitive plugins must fail closed.
279
331
  */
280
332
  actor: actorSchema.optional(),
281
- runId: z5.string().min(1),
333
+ runId: z6.string().min(1),
282
334
  source: sourceSchema,
283
- transcript: z5.array(pluginRunTranscriptEntrySchema)
335
+ transcript: z6.array(pluginRunTranscriptEntrySchema)
284
336
  }).strict();
285
337
 
286
338
  // src/tools.ts
287
- import { z as z6 } from "zod";
339
+ import { z as z7 } from "zod";
288
340
  var PluginToolInputError = class extends Error {
289
341
  constructor(message, options) {
290
342
  super(message, options);
291
343
  this.name = "PluginToolInputError";
292
344
  }
293
345
  };
294
- var pluginToolContentSchema = z6.discriminatedUnion("type", [
295
- z6.object({ type: z6.literal("text"), text: z6.string() }).strict(),
296
- z6.object({
297
- type: z6.literal("image"),
298
- data: z6.string(),
299
- mimeType: z6.string()
346
+ var pluginToolContentSchema = z7.discriminatedUnion("type", [
347
+ z7.object({ type: z7.literal("text"), text: z7.string() }).strict(),
348
+ z7.object({
349
+ type: z7.literal("image"),
350
+ data: z7.string(),
351
+ mimeType: z7.string()
300
352
  }).strict()
301
353
  ]);
302
- var pluginToolContinuationSchema = z6.object({
303
- arguments: z6.record(z6.string(), z6.unknown()),
304
- reason: z6.string().min(1).optional()
305
- }).strict();
306
- var pluginToolErrorSchema = z6.object({
307
- kind: z6.string().min(1),
308
- message: z6.string().min(1),
309
- retryable: z6.boolean().optional()
310
- }).strict();
311
- var pluginToolResultSchema = z6.object({
312
- ok: z6.boolean(),
313
- status: z6.enum(["success", "error"]),
314
- target: z6.string().min(1).optional(),
315
- data: z6.unknown().optional(),
316
- truncated: z6.boolean().optional(),
354
+ var pluginToolContinuationSchema = z7.object({
355
+ arguments: z7.record(z7.string(), z7.unknown()),
356
+ reason: z7.string().min(1).optional()
357
+ }).strict();
358
+ var pluginToolErrorSchema = z7.object({
359
+ kind: z7.string().min(1),
360
+ message: z7.string().min(1),
361
+ retryable: z7.boolean().optional()
362
+ }).strict();
363
+ var pluginToolResultSchema = z7.object({
364
+ ok: z7.boolean(),
365
+ status: z7.enum(["success", "error"]),
366
+ target: z7.string().min(1).optional(),
367
+ data: z7.unknown().optional(),
368
+ truncated: z7.boolean().optional(),
317
369
  continuation: pluginToolContinuationSchema.optional(),
318
- error: z6.union([pluginToolErrorSchema, z6.string()]).optional()
370
+ error: z7.union([pluginToolErrorSchema, z7.string()]).optional()
319
371
  }).passthrough();
320
- var toolApprovalModeSchema = z6.enum(["auto", "review", "approve"]);
372
+ var toolApprovalModeSchema = z7.enum(["auto", "review", "approve"]);
373
+ var REQUIRED_TOOL_ANNOTATION_KEYS = [
374
+ "destructiveHint",
375
+ "idempotentHint",
376
+ "openWorldHint",
377
+ "readOnlyHint"
378
+ ];
379
+ function missingToolAnnotationKeys(annotations) {
380
+ return REQUIRED_TOOL_ANNOTATION_KEYS.filter(
381
+ (key) => typeof annotations?.[key] !== "boolean"
382
+ );
383
+ }
321
384
  function isPluginToolResultEnvelope(value) {
322
385
  return value !== null && typeof value === "object" && Array.isArray(value.content) && "details" in value;
323
386
  }
@@ -342,7 +405,7 @@ function createZodTool(definition, helperName) {
342
405
  let modelInputSchema;
343
406
  let modelOutputSchema;
344
407
  try {
345
- modelInputSchema = z6.toJSONSchema(inputSchema);
408
+ modelInputSchema = z7.toJSONSchema(inputSchema);
346
409
  } catch (error) {
347
410
  throw new TypeError(
348
411
  `${helperName}() inputSchema must be representable as JSON Schema.`,
@@ -350,7 +413,7 @@ function createZodTool(definition, helperName) {
350
413
  );
351
414
  }
352
415
  try {
353
- modelOutputSchema = z6.toJSONSchema(outputSchema);
416
+ modelOutputSchema = z7.toJSONSchema(outputSchema);
354
417
  } catch (error) {
355
418
  throw new TypeError(
356
419
  `${helperName}() outputSchema must be representable as JSON Schema.`,
@@ -359,6 +422,7 @@ function createZodTool(definition, helperName) {
359
422
  }
360
423
  return {
361
424
  ...tool,
425
+ approvalMode: tool.approvalMode ?? "auto",
362
426
  inputSchema: modelInputSchema,
363
427
  outputSchema: modelOutputSchema,
364
428
  prepareArguments(args) {
@@ -375,7 +439,7 @@ function createZodTool(definition, helperName) {
375
439
  );
376
440
  if (isPluginToolResultEnvelope(result)) {
377
441
  return {
378
- content: z6.array(pluginToolContentSchema).parse(result.content),
442
+ content: z7.array(pluginToolContentSchema).parse(result.content),
379
443
  details: outputSchema.parse(
380
444
  pluginToolResultSchema.parse(result.details)
381
445
  )
@@ -394,74 +458,74 @@ function definePluginTool(definition) {
394
458
  }
395
459
 
396
460
  // src/operations.ts
397
- import { z as z7 } from "zod";
398
- var pluginApiRouteRequestContextSchema = z7.object({
399
- auth: z7.object({
400
- user: z7.object({
401
- email: z7.string().nullable().optional(),
402
- emailVerified: z7.boolean().optional(),
403
- name: z7.string().nullable().optional()
461
+ import { z as z8 } from "zod";
462
+ var pluginApiRouteRequestContextSchema = z8.object({
463
+ auth: z8.object({
464
+ user: z8.object({
465
+ email: z8.string().nullable().optional(),
466
+ emailVerified: z8.boolean().optional(),
467
+ name: z8.string().nullable().optional()
404
468
  }).strict()
405
469
  }).strict(),
406
470
  pluginName: nonBlankStringSchema
407
471
  }).strict();
408
472
 
409
473
  // src/credentials.ts
410
- import { z as z8 } from "zod";
411
- var pluginProviderNameSchema = z8.string().regex(/^[a-z][a-z0-9-]*$/);
412
- var pluginGrantNameSchema = z8.string().regex(/^[a-z][a-z0-9.-]*$/);
413
- var pluginGrantAccessSchema = z8.union([
414
- z8.literal("read"),
415
- z8.literal("write")
474
+ import { z as z9 } from "zod";
475
+ var pluginProviderNameSchema = z9.string().regex(/^[a-z][a-z0-9-]*$/);
476
+ var pluginGrantNameSchema = z9.string().regex(/^[a-z][a-z0-9.-]*$/);
477
+ var pluginGrantAccessSchema = z9.union([
478
+ z9.literal("read"),
479
+ z9.literal("write")
416
480
  ]);
417
- var pluginAuthorizationSchema = z8.object({
481
+ var pluginAuthorizationSchema = z9.object({
418
482
  provider: pluginProviderNameSchema,
419
483
  scope: nonBlankStringSchema.optional(),
420
- type: z8.literal("oauth")
484
+ type: z9.literal("oauth")
421
485
  }).strict();
422
- var pluginProviderAccountSchema = z8.object({
486
+ var pluginProviderAccountSchema = z9.object({
423
487
  id: nonBlankStringSchema,
424
488
  label: nonBlankStringSchema.optional(),
425
489
  url: nonBlankStringSchema.optional()
426
490
  }).strict();
427
- var pluginStoredTokensSchema = z8.object({
491
+ var pluginStoredTokensSchema = z9.object({
428
492
  account: pluginProviderAccountSchema.optional(),
429
493
  accessToken: nonBlankStringSchema,
430
- expiresAt: z8.number().finite().optional(),
494
+ expiresAt: z9.number().finite().optional(),
431
495
  refreshToken: nonBlankStringSchema,
432
- refreshTokenExpiresAt: z8.number().finite().optional(),
496
+ refreshTokenExpiresAt: z9.number().finite().optional(),
433
497
  scope: nonBlankStringSchema.optional()
434
498
  }).strict();
435
- var pluginGrantSchema = z8.object({
499
+ var pluginGrantSchema = z9.object({
436
500
  access: pluginGrantAccessSchema,
437
501
  leaseScope: nonBlankStringSchema.optional(),
438
502
  name: pluginGrantNameSchema,
439
503
  reason: nonBlankStringSchema.optional(),
440
- requirements: z8.array(nonBlankStringSchema).min(1).optional()
504
+ requirements: z9.array(nonBlankStringSchema).min(1).optional()
441
505
  }).strict();
442
- var pluginCredentialHeaderTransformSchema = z8.object({
443
- domain: z8.string().min(1),
444
- headers: z8.record(z8.string(), z8.string()).refine((headers) => Object.keys(headers).length > 0)
506
+ var pluginCredentialHeaderTransformSchema = z9.object({
507
+ domain: z9.string().min(1),
508
+ headers: z9.record(z9.string(), z9.string()).refine((headers) => Object.keys(headers).length > 0)
445
509
  }).strict();
446
- var pluginCredentialLeaseSchema = z8.object({
510
+ var pluginCredentialLeaseSchema = z9.object({
447
511
  account: pluginProviderAccountSchema.optional(),
448
512
  authorization: pluginAuthorizationSchema.optional(),
449
- expiresAt: z8.string().refine((value) => Number.isFinite(Date.parse(value))),
450
- headerTransforms: z8.array(pluginCredentialHeaderTransformSchema).min(1)
513
+ expiresAt: z9.string().refine((value) => Number.isFinite(Date.parse(value))),
514
+ headerTransforms: z9.array(pluginCredentialHeaderTransformSchema).min(1)
451
515
  }).strict();
452
- var pluginCredentialResultSchema = z8.discriminatedUnion("type", [
453
- z8.object({
516
+ var pluginCredentialResultSchema = z9.discriminatedUnion("type", [
517
+ z9.object({
454
518
  lease: pluginCredentialLeaseSchema,
455
- type: z8.literal("lease")
519
+ type: z9.literal("lease")
456
520
  }).strict(),
457
- z8.object({
521
+ z9.object({
458
522
  authorization: pluginAuthorizationSchema.optional(),
459
523
  message: nonBlankStringSchema,
460
- type: z8.literal("needed")
524
+ type: z9.literal("needed")
461
525
  }).strict(),
462
- z8.object({
526
+ z9.object({
463
527
  message: nonBlankStringSchema,
464
- type: z8.literal("unavailable")
528
+ type: z9.literal("unavailable")
465
529
  }).strict()
466
530
  ]);
467
531
  var EgressAuthRequired = class extends Error {
@@ -519,6 +583,26 @@ function defineJuniorPlugin(plugin) {
519
583
  if (plugin.userPages !== void 0 && !Array.isArray(plugin.userPages)) {
520
584
  throw new Error(`Junior plugin "${name}" userPages must be an array.`);
521
585
  }
586
+ if (plugin.conversationEvents !== void 0 && !Array.isArray(plugin.conversationEvents)) {
587
+ throw new Error(
588
+ `Junior plugin "${name}" conversationEvents must be an array.`
589
+ );
590
+ }
591
+ const conversationEventIds = /* @__PURE__ */ new Set();
592
+ for (const event of plugin.conversationEvents ?? []) {
593
+ if (!event || typeof event !== "object" && typeof event !== "function" || typeof event.parse !== "function" || typeof event.renderEvent !== "function") {
594
+ throw new Error(
595
+ `Junior plugin "${name}" conversation event definitions must be created with defineConversationEvent().`
596
+ );
597
+ }
598
+ const id = `${event.eventName}@${event.version}`;
599
+ if (conversationEventIds.has(id)) {
600
+ throw new Error(
601
+ `Junior plugin "${name}" has duplicate conversation event "${id}".`
602
+ );
603
+ }
604
+ conversationEventIds.add(id);
605
+ }
522
606
  const userPageIds = /* @__PURE__ */ new Set();
523
607
  for (const page of plugin.userPages ?? []) {
524
608
  if (!page || typeof page !== "object") {
@@ -552,56 +636,60 @@ function defineJuniorPlugin(plugin) {
552
636
  }
553
637
 
554
638
  // src/user-pages.ts
555
- import { z as z9 } from "zod";
639
+ import { z as z10 } from "zod";
556
640
  var userPageIdSchema = nonBlankStringSchema.max(64).regex(/^[a-z][a-z0-9-]*$/);
557
641
  var userPageLabelSchema = nonBlankStringSchema.max(80);
558
642
  var userPageDescriptionSchema = nonBlankStringSchema.max(500);
559
- var pluginUserPageMetadataSchema = z9.object({
643
+ var pluginUserPageMetadataSchema = z10.object({
560
644
  label: nonBlankStringSchema.max(80),
561
645
  value: nonBlankStringSchema.max(500)
562
646
  }).strict();
563
- var pluginUserPageActionSchema = z9.object({
647
+ var pluginUserPageActionSchema = z10.object({
564
648
  confirmation: nonBlankStringSchema.max(500).optional(),
565
649
  href: nonBlankStringSchema.max(500).regex(/^\/api\/plugins\/[a-z][a-z0-9-]*(?:\/|$)/),
566
650
  label: nonBlankStringSchema.max(80),
567
- method: z9.literal("DELETE"),
568
- tone: z9.enum(["danger", "neutral"]).optional()
651
+ method: z10.literal("DELETE"),
652
+ tone: z10.enum(["danger", "neutral"]).optional()
569
653
  }).strict();
570
- var pluginUserPageRecordSchema = z9.object({
571
- actions: z9.array(pluginUserPageActionSchema).max(4).optional(),
654
+ var pluginUserPageRecordSchema = z10.object({
655
+ actions: z10.array(pluginUserPageActionSchema).max(4).optional(),
572
656
  description: nonBlankStringSchema.max(1e3).optional(),
573
657
  id: nonBlankStringSchema.max(128),
574
- metadata: z9.array(pluginUserPageMetadataSchema).max(8).optional(),
658
+ metadata: z10.array(pluginUserPageMetadataSchema).max(8).optional(),
575
659
  title: nonBlankStringSchema.max(4e3)
576
660
  }).strict();
577
- var pluginUserPageContentSchema = z9.object({
661
+ var pluginUserPageContentSchema = z10.object({
578
662
  emptyText: nonBlankStringSchema.max(500).optional(),
579
663
  nextCursor: nonBlankStringSchema.max(1e3).optional(),
580
- records: z9.array(pluginUserPageRecordSchema).max(100),
664
+ records: z10.array(pluginUserPageRecordSchema).max(100),
581
665
  searchPlaceholder: nonBlankStringSchema.max(120).optional(),
582
- type: z9.literal("list")
666
+ type: z10.literal("list")
583
667
  }).strict();
584
- var pluginUserPageLinkSchema = z9.object({
668
+ var pluginUserPageLinkSchema = z10.object({
585
669
  description: userPageDescriptionSchema,
586
670
  id: userPageIdSchema,
587
671
  label: userPageLabelSchema,
588
672
  pluginDisplayName: nonBlankStringSchema.max(200),
589
673
  pluginName: nonBlankStringSchema.max(100)
590
674
  }).strict();
591
- var pluginUserPageLinksSchema = z9.array(pluginUserPageLinkSchema);
592
- var pluginUserPageInputSchema = z9.object({
675
+ var pluginUserPageLinksSchema = z10.array(pluginUserPageLinkSchema);
676
+ var pluginUserPageInputSchema = z10.object({
593
677
  cursor: nonBlankStringSchema.max(1e3).optional(),
594
- limit: z9.number().int().min(1).max(50),
595
- query: z9.string().trim().max(200).optional()
678
+ limit: z10.number().int().min(1).max(50),
679
+ query: z10.string().trim().max(200).optional()
596
680
  }).strict();
597
681
  export {
598
682
  EgressAuthRequired,
599
683
  EgressPolicyDenied,
600
684
  PluginToolInputError,
685
+ REQUIRED_TOOL_ANNOTATION_KEYS,
601
686
  actorSchema,
602
687
  conversationAnnotationInputSchema,
688
+ conversationEventIconSchema,
689
+ conversationEventPresentationSchema,
603
690
  createLocalSource,
604
691
  createSlackSource,
692
+ defineConversationEvent,
605
693
  defineJuniorPlugin,
606
694
  definePluginTool,
607
695
  definePromptContext,
@@ -614,6 +702,7 @@ export {
614
702
  localActorSchema,
615
703
  localDestinationSchema,
616
704
  localSourceSchema,
705
+ missingToolAnnotationKeys,
617
706
  nonBlankStringSchema,
618
707
  platformSchema,
619
708
  pluginApiRouteRequestContextSchema,
@@ -1,4 +1,5 @@
1
1
  import type { PluginCliDefinition } from "./cli";
2
+ import type { PluginConversationEventDefinition } from "./conversation-events";
2
3
  import type { PluginHooks } from "./hooks";
3
4
  import type { PluginManifest } from "./manifest";
4
5
  import type { PluginTasks } from "./tasks";
@@ -11,6 +12,7 @@ export interface PluginModelConfig {
11
12
  }
12
13
  export type PluginRegistrationInput = {
13
14
  cli?: PluginCliDefinition;
15
+ conversationEvents?: PluginConversationEventDefinition[];
14
16
  hooks?: PluginHooks;
15
17
  manifest: PluginManifest;
16
18
  model?: PluginModelConfig;
package/dist/tasks.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * scheduling, queue delivery, retries, and the bounded run projection.
6
6
  */
7
7
  import { z } from "zod";
8
+ import type { PluginConversationEvents } from "./conversation-events";
8
9
  import type { PluginContext, PluginEmbedder, PluginModel } from "./context";
9
10
  import type { PluginState } from "./state";
10
11
  /**
@@ -179,6 +180,7 @@ export type PluginRunContext = z.output<typeof pluginRunContextSchema>;
179
180
  /** Runtime context passed to a plugin-owned background task. */
180
181
  export interface PluginTaskContext extends PluginContext {
181
182
  embedder: PluginEmbedder;
183
+ events: PluginConversationEvents;
182
184
  id: string;
183
185
  model: PluginModel;
184
186
  name: string;
package/dist/tools.d.ts CHANGED
@@ -190,12 +190,11 @@ export type PluginToolExecute<TInput = unknown, TOutput = unknown> = {
190
190
  /**
191
191
  * Tool-declared approval mode.
192
192
  *
193
- * `auto` delegates to core policy, `review` enters approval review, and
194
- * `approve` permits execution without review. Omission delegates to core
195
- * defaults. These values do not select the reviewer.
193
+ * `auto` delegates to core policy, `review` enters Guardian review, and
194
+ * `approve` permits execution without review. Plugin tool helpers normalize
195
+ * omission to `auto`.
196
196
  *
197
- * This is declaration metadata only; current tool execution is unchanged.
198
- * TODO(#1053): Enforce effective approval modes before tool execution.
197
+ * Core resolves the effective mode immediately before execution.
199
198
  */
200
199
  export declare const toolApprovalModeSchema: z.ZodEnum<{
201
200
  auto: "auto";
@@ -206,7 +205,7 @@ export type ToolApprovalMode = z.output<typeof toolApprovalModeSchema>;
206
205
  /**
207
206
  * Reviewer signals describing a tool's side-effect behavior.
208
207
  *
209
- * These hints follow the MCP tool annotation contract. Approval review may use
208
+ * These hints follow the MCP tool annotation contract. Guardian may use
210
209
  * them as signals, but they never grant authority or override deterministic
211
210
  * authorization.
212
211
  */
@@ -218,18 +217,19 @@ export interface ToolAnnotations {
218
217
  readOnlyHint?: boolean;
219
218
  title?: string;
220
219
  }
220
+ export declare const REQUIRED_TOOL_ANNOTATION_KEYS: readonly ["destructiveHint", "idempotentHint", "openWorldHint", "readOnlyHint"];
221
+ export type RequiredToolAnnotationKey = (typeof REQUIRED_TOOL_ANNOTATION_KEYS)[number];
222
+ /** Return behavioral annotation keys that a tool did not declare. */
223
+ export declare function missingToolAnnotationKeys(annotations: ToolAnnotations | undefined): RequiredToolAnnotationKey[];
221
224
  /**
222
225
  * Canonical approval metadata declared by core and plugin tools.
223
- *
224
- * This metadata is declaration-only until #1053 adds approval enforcement.
225
- * Current tool execution is unchanged.
226
226
  */
227
227
  export interface ToolApprovalMetadata<TInput = unknown> {
228
- /** Optional declared approval mode; omission delegates to core defaults. */
228
+ /** Optional declared approval mode; the owning tool boundary selects defaults. */
229
229
  approvalMode?: ToolApprovalMode;
230
230
  annotations?: ToolAnnotations;
231
231
  /**
232
- * Describe the exact validated invocation for future approval presentation.
232
+ * Describe the reviewed semantic action for the review request.
233
233
  *
234
234
  * Core owns authoritative tool, actor, source, destination, conversation,
235
235
  * credential, and input data. This description adds domain-specific context
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-plugin-api",
3
- "version": "0.122.1",
3
+ "version": "0.123.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,110 @@
1
+ import { z } from "zod";
2
+
3
+ const conversationEventNameSchema = z
4
+ .string()
5
+ .trim()
6
+ .min(1)
7
+ .max(64)
8
+ .regex(/^[a-z][a-z0-9_]*$/);
9
+
10
+ export const conversationEventIconSchema = z.enum([
11
+ "activity",
12
+ "brain",
13
+ "calendar",
14
+ "check",
15
+ "database",
16
+ "info",
17
+ "key",
18
+ "link",
19
+ "sparkles",
20
+ "warning",
21
+ ]);
22
+
23
+ const conversationEventDetailSchema = z
24
+ .object({
25
+ description: z.string().trim().min(1).max(2_000).optional(),
26
+ metadata: z.array(z.string().trim().min(1).max(120)).max(8).optional(),
27
+ title: z.string().trim().min(1).max(4_000),
28
+ })
29
+ .strict();
30
+
31
+ /** Safe, core-rendered presentation for one plugin conversation event. */
32
+ export const conversationEventPresentationSchema = z
33
+ .object({
34
+ details: z.array(conversationEventDetailSchema).max(100).optional(),
35
+ icon: conversationEventIconSchema.optional(),
36
+ preview: z.string().trim().min(1).max(500).optional(),
37
+ title: z.string().trim().min(1).max(120),
38
+ })
39
+ .strict();
40
+
41
+ export type ConversationEventPresentation = z.output<
42
+ typeof conversationEventPresentationSchema
43
+ >;
44
+
45
+ /** One validated plugin event value waiting for conversation-bound emission. */
46
+ export interface PluginConversationEventValue {
47
+ readonly data: Record<string, unknown>;
48
+ readonly definition: PluginConversationEventDefinition;
49
+ }
50
+
51
+ /** Registered schema and transcript presentation for one plugin event version. */
52
+ export interface PluginConversationEventDefinition {
53
+ readonly eventName: string;
54
+ readonly version: number;
55
+ parse(data: unknown): Record<string, unknown>;
56
+ renderEvent(data: Record<string, unknown>): ConversationEventPresentation;
57
+ }
58
+
59
+ /** Typed factory returned while authoring one plugin conversation event. */
60
+ export interface DefinedConversationEvent<
61
+ TInput,
62
+ > extends PluginConversationEventDefinition {
63
+ (data: TInput): PluginConversationEventValue;
64
+ }
65
+
66
+ /** Define one typed, versioned plugin-owned conversation event. */
67
+ export function defineConversationEvent<
68
+ TSchema extends z.ZodType<Record<string, unknown>>,
69
+ >(definition: {
70
+ name: string;
71
+ version: number;
72
+ schema: TSchema;
73
+ renderEvent(
74
+ event: z.output<TSchema>,
75
+ ): z.input<typeof conversationEventPresentationSchema>;
76
+ }): DefinedConversationEvent<z.input<TSchema>> {
77
+ const identity = z
78
+ .object({
79
+ name: conversationEventNameSchema,
80
+ version: z.number().int().positive(),
81
+ })
82
+ .strict()
83
+ .parse({ name: definition.name, version: definition.version });
84
+ let eventDefinition: DefinedConversationEvent<z.input<TSchema>>;
85
+ const createEvent = (
86
+ data: z.input<TSchema>,
87
+ ): PluginConversationEventValue => ({
88
+ data: definition.schema.parse(data),
89
+ definition: eventDefinition,
90
+ });
91
+ eventDefinition = Object.assign(createEvent, {
92
+ eventName: identity.name,
93
+ version: identity.version,
94
+ parse(data: unknown) {
95
+ return definition.schema.parse(data);
96
+ },
97
+ renderEvent(data: Record<string, unknown>) {
98
+ const parsed = definition.schema.parse(data);
99
+ return conversationEventPresentationSchema.parse(
100
+ definition.renderEvent(parsed),
101
+ );
102
+ },
103
+ });
104
+ return eventDefinition;
105
+ }
106
+
107
+ /** Conversation-bound event writer supplied by Junior core. */
108
+ export interface PluginConversationEvents {
109
+ emit(event: PluginConversationEventValue): Promise<void>;
110
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./annotations";
2
+ export * from "./conversation-events";
2
3
  export * from "./schemas";
3
4
  export * from "./context";
4
5
  export * from "./state";
@@ -1,4 +1,5 @@
1
1
  import type { PluginCliDefinition } from "./cli";
2
+ import type { PluginConversationEventDefinition } from "./conversation-events";
2
3
  import type { PluginHooks } from "./hooks";
3
4
  import type { PluginManifest } from "./manifest";
4
5
  import type { PluginTasks } from "./tasks";
@@ -13,6 +14,7 @@ export interface PluginModelConfig {
13
14
 
14
15
  export type PluginRegistrationInput = {
15
16
  cli?: PluginCliDefinition;
17
+ conversationEvents?: PluginConversationEventDefinition[];
16
18
  hooks?: PluginHooks;
17
19
  manifest: PluginManifest;
18
20
  model?: PluginModelConfig;
@@ -72,6 +74,34 @@ export function defineJuniorPlugin(
72
74
  if (plugin.userPages !== undefined && !Array.isArray(plugin.userPages)) {
73
75
  throw new Error(`Junior plugin "${name}" userPages must be an array.`);
74
76
  }
77
+ if (
78
+ plugin.conversationEvents !== undefined &&
79
+ !Array.isArray(plugin.conversationEvents)
80
+ ) {
81
+ throw new Error(
82
+ `Junior plugin "${name}" conversationEvents must be an array.`,
83
+ );
84
+ }
85
+ const conversationEventIds = new Set<string>();
86
+ for (const event of plugin.conversationEvents ?? []) {
87
+ if (
88
+ !event ||
89
+ (typeof event !== "object" && typeof event !== "function") ||
90
+ typeof event.parse !== "function" ||
91
+ typeof event.renderEvent !== "function"
92
+ ) {
93
+ throw new Error(
94
+ `Junior plugin "${name}" conversation event definitions must be created with defineConversationEvent().`,
95
+ );
96
+ }
97
+ const id = `${event.eventName}@${event.version}`;
98
+ if (conversationEventIds.has(id)) {
99
+ throw new Error(
100
+ `Junior plugin "${name}" has duplicate conversation event "${id}".`,
101
+ );
102
+ }
103
+ conversationEventIds.add(id);
104
+ }
75
105
  const userPageIds = new Set<string>();
76
106
  for (const page of plugin.userPages ?? []) {
77
107
  if (!page || typeof page !== "object") {
package/src/tasks.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * scheduling, queue delivery, retries, and the bounded run projection.
6
6
  */
7
7
  import { z } from "zod";
8
+ import type { PluginConversationEvents } from "./conversation-events";
8
9
  import type { PluginContext, PluginEmbedder, PluginModel } from "./context";
9
10
  import { destinationSchema, actorSchema, sourceSchema } from "./schemas";
10
11
  import type { PluginState } from "./state";
@@ -81,6 +82,7 @@ export type PluginRunContext = z.output<typeof pluginRunContextSchema>;
81
82
  /** Runtime context passed to a plugin-owned background task. */
82
83
  export interface PluginTaskContext extends PluginContext {
83
84
  embedder: PluginEmbedder;
85
+ events: PluginConversationEvents;
84
86
  id: string;
85
87
  model: PluginModel;
86
88
  name: string;
package/src/tools.ts CHANGED
@@ -226,12 +226,11 @@ export type PluginToolExecute<TInput = unknown, TOutput = unknown> = {
226
226
  /**
227
227
  * Tool-declared approval mode.
228
228
  *
229
- * `auto` delegates to core policy, `review` enters approval review, and
230
- * `approve` permits execution without review. Omission delegates to core
231
- * defaults. These values do not select the reviewer.
229
+ * `auto` delegates to core policy, `review` enters Guardian review, and
230
+ * `approve` permits execution without review. Plugin tool helpers normalize
231
+ * omission to `auto`.
232
232
  *
233
- * This is declaration metadata only; current tool execution is unchanged.
234
- * TODO(#1053): Enforce effective approval modes before tool execution.
233
+ * Core resolves the effective mode immediately before execution.
235
234
  */
236
235
  export const toolApprovalModeSchema = z.enum(["auto", "review", "approve"]);
237
236
 
@@ -240,7 +239,7 @@ export type ToolApprovalMode = z.output<typeof toolApprovalModeSchema>;
240
239
  /**
241
240
  * Reviewer signals describing a tool's side-effect behavior.
242
241
  *
243
- * These hints follow the MCP tool annotation contract. Approval review may use
242
+ * These hints follow the MCP tool annotation contract. Guardian may use
244
243
  * them as signals, but they never grant authority or override deterministic
245
244
  * authorization.
246
245
  */
@@ -253,18 +252,34 @@ export interface ToolAnnotations {
253
252
  title?: string;
254
253
  }
255
254
 
255
+ export const REQUIRED_TOOL_ANNOTATION_KEYS = [
256
+ "destructiveHint",
257
+ "idempotentHint",
258
+ "openWorldHint",
259
+ "readOnlyHint",
260
+ ] as const;
261
+
262
+ export type RequiredToolAnnotationKey =
263
+ (typeof REQUIRED_TOOL_ANNOTATION_KEYS)[number];
264
+
265
+ /** Return behavioral annotation keys that a tool did not declare. */
266
+ export function missingToolAnnotationKeys(
267
+ annotations: ToolAnnotations | undefined,
268
+ ): RequiredToolAnnotationKey[] {
269
+ return REQUIRED_TOOL_ANNOTATION_KEYS.filter(
270
+ (key) => typeof annotations?.[key] !== "boolean",
271
+ );
272
+ }
273
+
256
274
  /**
257
275
  * Canonical approval metadata declared by core and plugin tools.
258
- *
259
- * This metadata is declaration-only until #1053 adds approval enforcement.
260
- * Current tool execution is unchanged.
261
276
  */
262
277
  export interface ToolApprovalMetadata<TInput = unknown> {
263
- /** Optional declared approval mode; omission delegates to core defaults. */
278
+ /** Optional declared approval mode; the owning tool boundary selects defaults. */
264
279
  approvalMode?: ToolApprovalMode;
265
280
  annotations?: ToolAnnotations;
266
281
  /**
267
- * Describe the exact validated invocation for future approval presentation.
282
+ * Describe the reviewed semantic action for the review request.
268
283
  *
269
284
  * Core owns authoritative tool, actor, source, destination, conversation,
270
285
  * credential, and input data. This description adds domain-specific context
@@ -403,6 +418,7 @@ function createZodTool<
403
418
  }
404
419
  return {
405
420
  ...tool,
421
+ approvalMode: tool.approvalMode ?? "auto",
406
422
  inputSchema: modelInputSchema,
407
423
  outputSchema: modelOutputSchema,
408
424
  prepareArguments(args) {