@sentry/junior-plugin-api 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,40 @@
1
- export interface AgentPluginRequester {
2
- userId?: string;
3
- userName?: string;
4
- fullName?: string;
5
- email?: string;
6
- }
1
+ import { z } from "zod";
2
+ /** Runtime-owned provider-neutral address for routing future work or side effects. */
3
+ export declare const destinationSchema: z.ZodObject<{
4
+ platform: z.ZodLiteral<"slack">;
5
+ teamId: z.ZodString;
6
+ channelId: z.ZodString;
7
+ }, z.core.$strict>;
8
+ /** Stable user credential subject shape accepted from plugins. */
9
+ export declare const agentPluginCredentialSubjectSchema: z.ZodObject<{
10
+ type: z.ZodLiteral<"user">;
11
+ userId: z.ZodString;
12
+ allowedWhen: z.ZodLiteral<"private-direct-conversation">;
13
+ }, z.core.$strict>;
14
+ /** Runtime-provided requester identity visible to plugin hooks. */
15
+ export declare const agentPluginRequesterSchema: z.ZodObject<{
16
+ userId: z.ZodOptional<z.ZodString>;
17
+ userName: z.ZodOptional<z.ZodString>;
18
+ fullName: z.ZodOptional<z.ZodString>;
19
+ email: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strict>;
21
+ /** Plugin dispatch request accepted by Junior core. */
22
+ export declare const dispatchOptionsSchema: z.ZodObject<{
23
+ idempotencyKey: z.ZodPipe<z.ZodString, z.ZodString>;
24
+ credentialSubject: z.ZodOptional<z.ZodObject<{
25
+ type: z.ZodLiteral<"user">;
26
+ userId: z.ZodString;
27
+ allowedWhen: z.ZodLiteral<"private-direct-conversation">;
28
+ }, z.core.$strict>>;
29
+ destination: z.ZodObject<{
30
+ platform: z.ZodLiteral<"slack">;
31
+ teamId: z.ZodString;
32
+ channelId: z.ZodString;
33
+ }, z.core.$strict>;
34
+ input: z.ZodPipe<z.ZodString, z.ZodString>;
35
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
36
+ }, z.core.$strict>;
37
+ export type AgentPluginRequester = z.output<typeof agentPluginRequesterSchema>;
7
38
  export interface AgentPluginMetadata {
8
39
  name: string;
9
40
  }
@@ -20,7 +51,7 @@ export interface AgentPluginLogger {
20
51
  info(message: string, metadata?: Record<string, unknown>): void;
21
52
  warn(message: string, metadata?: Record<string, unknown>): void;
22
53
  }
23
- /** Thrown when a trusted plugin tool rejects invalid model or user input. */
54
+ /** Thrown when a plugin tool rejects invalid model or user input. */
24
55
  export declare class AgentPluginToolInputError extends Error {
25
56
  constructor(message: string, options?: {
26
57
  cause?: unknown;
@@ -90,13 +121,36 @@ export interface AgentPluginToolDefinition<TInput = unknown> {
90
121
  execute?: AgentPluginToolExecute<TInput>;
91
122
  }
92
123
  export interface ToolRegistrationHookContext extends AgentPluginContext {
124
+ /**
125
+ * Capabilities of `channelId` — the raw conversation channel exposed to
126
+ * this plugin. Recomputed from `channelId`, not from `destination`.
127
+ */
93
128
  channelCapabilities?: {
94
129
  canAddReactions: boolean;
95
130
  canCreateCanvas: boolean;
96
131
  canPostToChannel: boolean;
97
132
  };
133
+ /**
134
+ * The raw Slack channel ID for this conversation — the DM or channel where
135
+ * this turn is happening, without any assistant-context-source override.
136
+ * Use this as the stable binding key for state scoped to a Slack conversation.
137
+ * `channelCapabilities` describes this channel.
138
+ */
98
139
  channelId?: string;
140
+ /**
141
+ * Opaque Junior conversation/session identity for this turn.
142
+ * Interactive Slack turns use `slack:{channelId}:{threadTs}`.
143
+ * Scheduled/API turns use an internal id such as `agent-dispatch:{id}`.
144
+ * Do not parse as Slack unless the value starts with `slack:`.
145
+ */
146
+ conversationId?: string;
99
147
  credentialSubject?: AgentPluginCredentialSubject;
148
+ /**
149
+ * Runtime-owned destination suitable for future autonomous dispatch. For
150
+ * Slack, this is the raw conversation channel, not a thread timestamp or
151
+ * assistant-context source channel.
152
+ */
153
+ destination?: Destination;
100
154
  messageTs?: string;
101
155
  requester?: AgentPluginRequester;
102
156
  state: AgentPluginState;
@@ -104,22 +158,9 @@ export interface ToolRegistrationHookContext extends AgentPluginContext {
104
158
  threadTs?: string;
105
159
  userText?: string;
106
160
  }
107
- export interface AgentPluginCredentialSubject {
108
- type: "user";
109
- userId: string;
110
- allowedWhen: "private-direct-conversation";
111
- }
112
- export interface DispatchOptions {
113
- credentialSubject?: AgentPluginCredentialSubject;
114
- destination: {
115
- platform: "slack";
116
- teamId: string;
117
- channelId: string;
118
- };
119
- idempotencyKey: string;
120
- input: string;
121
- metadata?: Record<string, string>;
122
- }
161
+ export type AgentPluginCredentialSubject = z.output<typeof agentPluginCredentialSubjectSchema>;
162
+ export type Destination = z.output<typeof destinationSchema>;
163
+ export type DispatchOptions = z.output<typeof dispatchOptionsSchema>;
123
164
  export interface DispatchResult {
124
165
  id: string;
125
166
  status: "created" | "already_exists";
@@ -202,9 +243,166 @@ export interface SlackConversationLink {
202
243
  export interface SlackConversationLinkHookContext extends AgentPluginContext {
203
244
  conversationId: string;
204
245
  }
246
+ declare const agentPluginGrantAccessSchema: z.ZodUnion<readonly [z.ZodLiteral<"read">, z.ZodLiteral<"write">]>;
247
+ /** Runtime schema for provider authorization a plugin may request. */
248
+ export declare const agentPluginAuthorizationSchema: z.ZodObject<{
249
+ provider: z.ZodString;
250
+ scope: z.ZodOptional<z.ZodString>;
251
+ type: z.ZodLiteral<"oauth">;
252
+ }, z.core.$strict>;
253
+ /** Runtime schema for a provider account attached to stored OAuth tokens. */
254
+ export declare const agentPluginProviderAccountSchema: z.ZodObject<{
255
+ id: z.ZodString;
256
+ label: z.ZodOptional<z.ZodString>;
257
+ url: z.ZodOptional<z.ZodString>;
258
+ }, z.core.$strict>;
259
+ /** Runtime schema for a plugin-defined outbound credential grant. */
260
+ export declare const agentPluginGrantSchema: z.ZodObject<{
261
+ access: z.ZodUnion<readonly [z.ZodLiteral<"read">, z.ZodLiteral<"write">]>;
262
+ name: z.ZodString;
263
+ reason: z.ZodOptional<z.ZodString>;
264
+ requirements: z.ZodOptional<z.ZodArray<z.ZodString>>;
265
+ }, z.core.$strict>;
266
+ /** Runtime schema for plugin-issued header mutations. */
267
+ export declare const agentPluginCredentialHeaderTransformSchema: z.ZodObject<{
268
+ domain: z.ZodString;
269
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
270
+ }, z.core.$strict>;
271
+ /** Runtime schema for a short-lived plugin-issued credential lease. */
272
+ export declare const agentPluginCredentialLeaseSchema: z.ZodObject<{
273
+ account: z.ZodOptional<z.ZodObject<{
274
+ id: z.ZodString;
275
+ label: z.ZodOptional<z.ZodString>;
276
+ url: z.ZodOptional<z.ZodString>;
277
+ }, z.core.$strict>>;
278
+ authorization: z.ZodOptional<z.ZodObject<{
279
+ provider: z.ZodString;
280
+ scope: z.ZodOptional<z.ZodString>;
281
+ type: z.ZodLiteral<"oauth">;
282
+ }, z.core.$strict>>;
283
+ expiresAt: z.ZodString;
284
+ headerTransforms: z.ZodArray<z.ZodObject<{
285
+ domain: z.ZodString;
286
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
287
+ }, z.core.$strict>>;
288
+ }, z.core.$strict>;
289
+ /** Runtime schema for the result returned by a plugin credential hook. */
290
+ export declare const agentPluginCredentialResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
291
+ lease: z.ZodObject<{
292
+ account: z.ZodOptional<z.ZodObject<{
293
+ id: z.ZodString;
294
+ label: z.ZodOptional<z.ZodString>;
295
+ url: z.ZodOptional<z.ZodString>;
296
+ }, z.core.$strict>>;
297
+ authorization: z.ZodOptional<z.ZodObject<{
298
+ provider: z.ZodString;
299
+ scope: z.ZodOptional<z.ZodString>;
300
+ type: z.ZodLiteral<"oauth">;
301
+ }, z.core.$strict>>;
302
+ expiresAt: z.ZodString;
303
+ headerTransforms: z.ZodArray<z.ZodObject<{
304
+ domain: z.ZodString;
305
+ headers: z.ZodRecord<z.ZodString, z.ZodString>;
306
+ }, z.core.$strict>>;
307
+ }, z.core.$strict>;
308
+ type: z.ZodLiteral<"lease">;
309
+ }, z.core.$strict>, z.ZodObject<{
310
+ authorization: z.ZodOptional<z.ZodObject<{
311
+ provider: z.ZodString;
312
+ scope: z.ZodOptional<z.ZodString>;
313
+ type: z.ZodLiteral<"oauth">;
314
+ }, z.core.$strict>>;
315
+ message: z.ZodString;
316
+ type: z.ZodLiteral<"needed">;
317
+ }, z.core.$strict>, z.ZodObject<{
318
+ message: z.ZodString;
319
+ type: z.ZodLiteral<"unavailable">;
320
+ }, z.core.$strict>], "type">;
321
+ export type AgentPluginGrantAccess = z.output<typeof agentPluginGrantAccessSchema>;
322
+ /** Provider authorization Junior can start when a plugin-owned grant is missing. */
323
+ export type AgentPluginAuthorization = z.output<typeof agentPluginAuthorizationSchema>;
324
+ /** Interrupt sandbox egress so Junior can start provider authorization. */
325
+ export declare class EgressAuthRequired extends Error {
326
+ authorization?: AgentPluginAuthorization;
327
+ constructor(message: string, options?: {
328
+ authorization?: AgentPluginAuthorization;
329
+ cause?: unknown;
330
+ });
331
+ }
332
+ /** Provider account identity resolved by a plugin OAuth hook. */
333
+ export type AgentPluginProviderAccount = z.output<typeof agentPluginProviderAccountSchema>;
334
+ /** Plugin-defined grant required before Junior can forward one outbound request. */
335
+ export type AgentPluginGrant = z.output<typeof agentPluginGrantSchema>;
336
+ /** Request details available while selecting the grant for sandbox egress. */
337
+ export interface AgentPluginEgressRequest {
338
+ /** Capped request body text when the host exposes it for provider-specific grant classification. */
339
+ bodyText?: string;
340
+ method: string;
341
+ url: string;
342
+ }
343
+ export interface EgressHookContext extends AgentPluginContext {
344
+ request: AgentPluginEgressRequest;
345
+ }
346
+ export interface AgentPluginEgressResponse {
347
+ /** Snapshot of upstream response headers; mutations do not affect pass-through. */
348
+ headers: Headers;
349
+ readText(maxBytes: number): Promise<string | undefined>;
350
+ status: number;
351
+ }
352
+ export interface EgressResponseHookContext extends AgentPluginContext {
353
+ grant: AgentPluginGrant;
354
+ permissionDenied(message: string): void;
355
+ request: Omit<AgentPluginEgressRequest, "bodyText">;
356
+ response: AgentPluginEgressResponse;
357
+ }
358
+ /** Header mutations a plugin-issued credential lease may apply to owned domains. */
359
+ export type AgentPluginCredentialHeaderTransform = z.output<typeof agentPluginCredentialHeaderTransformSchema>;
360
+ /** Short-lived credential headers issued by a plugin for a selected grant. */
361
+ export type AgentPluginCredentialLease = z.output<typeof agentPluginCredentialLeaseSchema>;
362
+ export type AgentPluginCredentialResult = z.output<typeof agentPluginCredentialResultSchema>;
363
+ export type AgentPluginCredentialActor = {
364
+ type: "system";
365
+ id: string;
366
+ } | {
367
+ type: "user";
368
+ userId: string;
369
+ };
370
+ export interface AgentPluginResolvedCredentialUser {
371
+ type: "user";
372
+ userId: string;
373
+ }
374
+ export interface AgentPluginStoredTokens {
375
+ account?: AgentPluginProviderAccount;
376
+ accessToken: string;
377
+ expiresAt?: number;
378
+ refreshToken: string;
379
+ scope?: string;
380
+ }
381
+ export interface AgentPluginUserTokenSlot {
382
+ get(): Promise<AgentPluginStoredTokens | undefined>;
383
+ set(tokens: AgentPluginStoredTokens): Promise<void>;
384
+ userId: string;
385
+ }
386
+ export interface AgentPluginTokenStore {
387
+ credentialSubject?: AgentPluginUserTokenSlot;
388
+ currentUser?: AgentPluginUserTokenSlot;
389
+ }
390
+ export interface ResolveOAuthAccountHookContext extends AgentPluginContext {
391
+ tokens: AgentPluginStoredTokens;
392
+ }
393
+ export interface IssueCredentialHookContext extends AgentPluginContext {
394
+ actor: AgentPluginCredentialActor;
395
+ credentialSubject?: AgentPluginResolvedCredentialUser;
396
+ grant: AgentPluginGrant;
397
+ tokens: AgentPluginTokenStore;
398
+ }
205
399
  export interface AgentPluginHooks {
206
400
  sandboxPrepare?(ctx: SandboxPrepareHookContext): Promise<void> | void;
207
401
  beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
402
+ grantForEgress?(ctx: EgressHookContext): Promise<AgentPluginGrant | undefined> | AgentPluginGrant | undefined;
403
+ issueCredential?(ctx: IssueCredentialHookContext): Promise<AgentPluginCredentialResult> | AgentPluginCredentialResult;
404
+ onEgressResponse?(ctx: EgressResponseHookContext): Promise<void> | void;
405
+ resolveOAuthAccount?(ctx: ResolveOAuthAccountHookContext): Promise<AgentPluginProviderAccount | undefined> | AgentPluginProviderAccount | undefined;
208
406
  routes?(ctx: RouteRegistrationHookContext): AgentPluginRoute[];
209
407
  tools?(ctx: ToolRegistrationHookContext): Record<string, AgentPluginToolDefinition>;
210
408
  heartbeat?(ctx: HeartbeatHookContext): Promise<HeartbeatResult | void> | HeartbeatResult | void;
@@ -217,6 +415,21 @@ export interface JuniorPluginOAuthConfig {
217
415
  clientIdEnv: string;
218
416
  clientSecretEnv: string;
219
417
  scope?: string;
418
+ /**
419
+ * Treat a provider token response with `scope: ""` like an omitted scope and
420
+ * fall back to the requested scope string when storing the token.
421
+ *
422
+ * Enable this only for providers whose token responses cannot report OAuth
423
+ * scopes even though Junior needs a local requested-scope string for
424
+ * reauthorization checks. The built-in GitHub App plugin enables this because
425
+ * GitHub App user-to-server tokens always return an empty scope value — their
426
+ * effective access is enforced by GitHub App permissions, installation
427
+ * repository access, and the requesting user's own access, not OAuth scopes.
428
+ *
429
+ * Do not enable this for standard OAuth providers where an explicit empty
430
+ * `scope` means the provider granted no scopes.
431
+ */
432
+ treatEmptyScopeAsUnreported?: boolean;
220
433
  tokenAuthMethod?: "body" | "basic";
221
434
  tokenEndpoint: string;
222
435
  tokenExtraHeaders?: Record<string, string>;
@@ -228,17 +441,7 @@ export interface JuniorPluginOAuthBearerCredentials {
228
441
  domains: string[];
229
442
  type: "oauth-bearer";
230
443
  }
231
- export interface JuniorPluginGitHubAppCredentials {
232
- apiHeaders?: Record<string, string>;
233
- appIdEnv: string;
234
- authTokenEnv: string;
235
- authTokenPlaceholder?: string;
236
- domains: string[];
237
- installationIdEnv: string;
238
- privateKeyEnv: string;
239
- type: "github-app";
240
- }
241
- export type JuniorPluginCredentials = JuniorPluginOAuthBearerCredentials | JuniorPluginGitHubAppCredentials;
444
+ export type JuniorPluginCredentials = JuniorPluginOAuthBearerCredentials;
242
445
  export interface JuniorPluginNpmRuntimeDependency {
243
446
  package: string;
244
447
  type: "npm";
@@ -301,3 +504,4 @@ export interface JuniorPluginRegistration extends JuniorPluginRegistrationInput
301
504
  }
302
505
  /** Define one Junior plugin registration for app and build-time wiring. */
303
506
  export declare function defineJuniorPlugin(plugin: JuniorPluginRegistrationInput): JuniorPluginRegistration;
507
+ export {};
package/dist/index.js CHANGED
@@ -1,15 +1,134 @@
1
1
  // src/index.ts
2
+ import { z } from "zod";
3
+ var slackTeamIdSchema = z.string().regex(/^T[A-Z0-9]+$/);
4
+ var slackConversationIdSchema = z.string().regex(/^(C|G|D)[A-Z0-9]+$/);
5
+ var exactActorUserIdSchema = z.string().min(1).refine(
6
+ (value) => value === value.trim() && value.toLowerCase() !== "unknown"
7
+ );
8
+ var nonBlankStringSchema = z.string().refine((value) => value.trim().length > 0);
9
+ var destinationSchema = z.object({
10
+ platform: z.literal("slack"),
11
+ teamId: slackTeamIdSchema,
12
+ channelId: slackConversationIdSchema
13
+ }).strict();
14
+ var agentPluginCredentialSubjectSchema = z.object({
15
+ type: z.literal("user"),
16
+ userId: exactActorUserIdSchema,
17
+ allowedWhen: z.literal("private-direct-conversation")
18
+ }).strict();
19
+ var agentPluginRequesterSchema = z.object({
20
+ userId: exactActorUserIdSchema.optional(),
21
+ userName: nonBlankStringSchema.optional(),
22
+ fullName: nonBlankStringSchema.optional(),
23
+ email: nonBlankStringSchema.optional()
24
+ }).strict();
25
+ var dispatchMetadataSchema = z.record(z.string(), z.string()).superRefine((metadata, ctx) => {
26
+ const entries = Object.entries(metadata);
27
+ if (entries.length > 20) {
28
+ ctx.addIssue({
29
+ code: z.ZodIssueCode.custom,
30
+ message: "Dispatch metadata has too many keys"
31
+ });
32
+ return;
33
+ }
34
+ for (const [key, value] of entries) {
35
+ if (!key.trim()) {
36
+ ctx.addIssue({
37
+ code: z.ZodIssueCode.custom,
38
+ message: "Dispatch metadata values must be strings",
39
+ path: [key]
40
+ });
41
+ continue;
42
+ }
43
+ if (key.length > 128) {
44
+ ctx.addIssue({
45
+ code: z.ZodIssueCode.custom,
46
+ message: "Dispatch metadata key exceeds the maximum length",
47
+ path: [key]
48
+ });
49
+ }
50
+ if (value.length > 512) {
51
+ ctx.addIssue({
52
+ code: z.ZodIssueCode.custom,
53
+ message: "Dispatch metadata value exceeds the maximum length",
54
+ path: [key]
55
+ });
56
+ }
57
+ }
58
+ });
59
+ var dispatchOptionsSchema = z.object({
60
+ idempotencyKey: nonBlankStringSchema.pipe(z.string().max(512)),
61
+ credentialSubject: agentPluginCredentialSubjectSchema.optional(),
62
+ destination: destinationSchema,
63
+ input: nonBlankStringSchema.pipe(z.string().max(32e3)),
64
+ metadata: dispatchMetadataSchema.optional()
65
+ }).strict();
2
66
  var AgentPluginToolInputError = class extends Error {
3
67
  constructor(message, options) {
4
68
  super(message, options);
5
69
  this.name = "AgentPluginToolInputError";
6
70
  }
7
71
  };
72
+ var agentPluginProviderNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
73
+ var agentPluginGrantNameSchema = z.string().regex(/^[a-z][a-z0-9.-]*$/);
74
+ var agentPluginGrantAccessSchema = z.union([
75
+ z.literal("read"),
76
+ z.literal("write")
77
+ ]);
78
+ var agentPluginAuthorizationSchema = z.object({
79
+ provider: agentPluginProviderNameSchema,
80
+ scope: nonBlankStringSchema.optional(),
81
+ type: z.literal("oauth")
82
+ }).strict();
83
+ var agentPluginProviderAccountSchema = z.object({
84
+ id: nonBlankStringSchema,
85
+ label: nonBlankStringSchema.optional(),
86
+ url: nonBlankStringSchema.optional()
87
+ }).strict();
88
+ var agentPluginGrantSchema = z.object({
89
+ access: agentPluginGrantAccessSchema,
90
+ name: agentPluginGrantNameSchema,
91
+ reason: nonBlankStringSchema.optional(),
92
+ requirements: z.array(nonBlankStringSchema).min(1).optional()
93
+ }).strict();
94
+ var agentPluginCredentialHeaderTransformSchema = z.object({
95
+ domain: z.string().min(1),
96
+ headers: z.record(z.string(), z.string()).refine((headers) => Object.keys(headers).length > 0)
97
+ }).strict();
98
+ var agentPluginCredentialLeaseSchema = z.object({
99
+ account: agentPluginProviderAccountSchema.optional(),
100
+ authorization: agentPluginAuthorizationSchema.optional(),
101
+ expiresAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
102
+ headerTransforms: z.array(agentPluginCredentialHeaderTransformSchema).min(1)
103
+ }).strict();
104
+ var agentPluginCredentialResultSchema = z.discriminatedUnion("type", [
105
+ z.object({
106
+ lease: agentPluginCredentialLeaseSchema,
107
+ type: z.literal("lease")
108
+ }).strict(),
109
+ z.object({
110
+ authorization: agentPluginAuthorizationSchema.optional(),
111
+ message: nonBlankStringSchema,
112
+ type: z.literal("needed")
113
+ }).strict(),
114
+ z.object({
115
+ message: nonBlankStringSchema,
116
+ type: z.literal("unavailable")
117
+ }).strict()
118
+ ]);
119
+ var EgressAuthRequired = class extends Error {
120
+ authorization;
121
+ constructor(message, options) {
122
+ super(message, { cause: options?.cause });
123
+ this.name = "EgressAuthRequired";
124
+ this.authorization = options?.authorization;
125
+ }
126
+ };
8
127
  var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
9
128
  function defineJuniorPlugin(plugin) {
10
129
  if ("pluginConfig" in plugin) {
11
130
  throw new Error(
12
- "pluginConfig is no longer supported. Put runtime metadata in manifest and trusted state prefixes on the plugin registration."
131
+ "pluginConfig is no longer supported. Put runtime metadata in manifest and state prefixes on the plugin registration."
13
132
  );
14
133
  }
15
134
  const manifest = plugin.manifest;
@@ -46,5 +165,16 @@ function defineJuniorPlugin(plugin) {
46
165
  }
47
166
  export {
48
167
  AgentPluginToolInputError,
49
- defineJuniorPlugin
168
+ EgressAuthRequired,
169
+ agentPluginAuthorizationSchema,
170
+ agentPluginCredentialHeaderTransformSchema,
171
+ agentPluginCredentialLeaseSchema,
172
+ agentPluginCredentialResultSchema,
173
+ agentPluginCredentialSubjectSchema,
174
+ agentPluginGrantSchema,
175
+ agentPluginProviderAccountSchema,
176
+ agentPluginRequesterSchema,
177
+ defineJuniorPlugin,
178
+ destinationSchema,
179
+ dispatchOptionsSchema
50
180
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-plugin-api",
3
- "version": "0.68.0",
3
+ "version": "0.70.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -21,6 +21,9 @@
21
21
  "dist",
22
22
  "src"
23
23
  ],
24
+ "dependencies": {
25
+ "zod": "^4.4.3"
26
+ },
24
27
  "devDependencies": {
25
28
  "oxlint": "^1.66.0",
26
29
  "tsup": "^8.5.1",
package/src/index.ts CHANGED
@@ -1,9 +1,94 @@
1
- export interface AgentPluginRequester {
2
- userId?: string;
3
- userName?: string;
4
- fullName?: string;
5
- email?: string;
6
- }
1
+ import { z } from "zod";
2
+
3
+ const slackTeamIdSchema = z.string().regex(/^T[A-Z0-9]+$/);
4
+ const slackConversationIdSchema = z.string().regex(/^(C|G|D)[A-Z0-9]+$/);
5
+ const exactActorUserIdSchema = z
6
+ .string()
7
+ .min(1)
8
+ .refine(
9
+ (value) => value === value.trim() && value.toLowerCase() !== "unknown",
10
+ );
11
+ const nonBlankStringSchema = z
12
+ .string()
13
+ .refine((value) => value.trim().length > 0);
14
+
15
+ /** Runtime-owned provider-neutral address for routing future work or side effects. */
16
+ export const destinationSchema = z
17
+ .object({
18
+ platform: z.literal("slack"),
19
+ teamId: slackTeamIdSchema,
20
+ channelId: slackConversationIdSchema,
21
+ })
22
+ .strict();
23
+
24
+ /** Stable user credential subject shape accepted from plugins. */
25
+ export const agentPluginCredentialSubjectSchema = z
26
+ .object({
27
+ type: z.literal("user"),
28
+ userId: exactActorUserIdSchema,
29
+ allowedWhen: z.literal("private-direct-conversation"),
30
+ })
31
+ .strict();
32
+
33
+ /** Runtime-provided requester identity visible to plugin hooks. */
34
+ export const agentPluginRequesterSchema = z
35
+ .object({
36
+ userId: exactActorUserIdSchema.optional(),
37
+ userName: nonBlankStringSchema.optional(),
38
+ fullName: nonBlankStringSchema.optional(),
39
+ email: nonBlankStringSchema.optional(),
40
+ })
41
+ .strict();
42
+
43
+ const dispatchMetadataSchema = z
44
+ .record(z.string(), z.string())
45
+ .superRefine((metadata, ctx) => {
46
+ const entries = Object.entries(metadata);
47
+ if (entries.length > 20) {
48
+ ctx.addIssue({
49
+ code: z.ZodIssueCode.custom,
50
+ message: "Dispatch metadata has too many keys",
51
+ });
52
+ return;
53
+ }
54
+ for (const [key, value] of entries) {
55
+ if (!key.trim()) {
56
+ ctx.addIssue({
57
+ code: z.ZodIssueCode.custom,
58
+ message: "Dispatch metadata values must be strings",
59
+ path: [key],
60
+ });
61
+ continue;
62
+ }
63
+ if (key.length > 128) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ message: "Dispatch metadata key exceeds the maximum length",
67
+ path: [key],
68
+ });
69
+ }
70
+ if (value.length > 512) {
71
+ ctx.addIssue({
72
+ code: z.ZodIssueCode.custom,
73
+ message: "Dispatch metadata value exceeds the maximum length",
74
+ path: [key],
75
+ });
76
+ }
77
+ }
78
+ });
79
+
80
+ /** Plugin dispatch request accepted by Junior core. */
81
+ export const dispatchOptionsSchema = z
82
+ .object({
83
+ idempotencyKey: nonBlankStringSchema.pipe(z.string().max(512)),
84
+ credentialSubject: agentPluginCredentialSubjectSchema.optional(),
85
+ destination: destinationSchema,
86
+ input: nonBlankStringSchema.pipe(z.string().max(32_000)),
87
+ metadata: dispatchMetadataSchema.optional(),
88
+ })
89
+ .strict();
90
+
91
+ export type AgentPluginRequester = z.output<typeof agentPluginRequesterSchema>;
7
92
 
8
93
  export interface AgentPluginMetadata {
9
94
  name: string;
@@ -25,7 +110,7 @@ export interface AgentPluginLogger {
25
110
  warn(message: string, metadata?: Record<string, unknown>): void;
26
111
  }
27
112
 
28
- /** Thrown when a trusted plugin tool rejects invalid model or user input. */
113
+ /** Thrown when a plugin tool rejects invalid model or user input. */
29
114
  export class AgentPluginToolInputError extends Error {
30
115
  constructor(message: string, options?: { cause?: unknown }) {
31
116
  super(message, options);
@@ -104,13 +189,36 @@ export interface AgentPluginToolDefinition<TInput = unknown> {
104
189
  }
105
190
 
106
191
  export interface ToolRegistrationHookContext extends AgentPluginContext {
192
+ /**
193
+ * Capabilities of `channelId` — the raw conversation channel exposed to
194
+ * this plugin. Recomputed from `channelId`, not from `destination`.
195
+ */
107
196
  channelCapabilities?: {
108
197
  canAddReactions: boolean;
109
198
  canCreateCanvas: boolean;
110
199
  canPostToChannel: boolean;
111
200
  };
201
+ /**
202
+ * The raw Slack channel ID for this conversation — the DM or channel where
203
+ * this turn is happening, without any assistant-context-source override.
204
+ * Use this as the stable binding key for state scoped to a Slack conversation.
205
+ * `channelCapabilities` describes this channel.
206
+ */
112
207
  channelId?: string;
208
+ /**
209
+ * Opaque Junior conversation/session identity for this turn.
210
+ * Interactive Slack turns use `slack:{channelId}:{threadTs}`.
211
+ * Scheduled/API turns use an internal id such as `agent-dispatch:{id}`.
212
+ * Do not parse as Slack unless the value starts with `slack:`.
213
+ */
214
+ conversationId?: string;
113
215
  credentialSubject?: AgentPluginCredentialSubject;
216
+ /**
217
+ * Runtime-owned destination suitable for future autonomous dispatch. For
218
+ * Slack, this is the raw conversation channel, not a thread timestamp or
219
+ * assistant-context source channel.
220
+ */
221
+ destination?: Destination;
114
222
  messageTs?: string;
115
223
  requester?: AgentPluginRequester;
116
224
  state: AgentPluginState;
@@ -119,23 +227,13 @@ export interface ToolRegistrationHookContext extends AgentPluginContext {
119
227
  userText?: string;
120
228
  }
121
229
 
122
- export interface AgentPluginCredentialSubject {
123
- type: "user";
124
- userId: string;
125
- allowedWhen: "private-direct-conversation";
126
- }
230
+ export type AgentPluginCredentialSubject = z.output<
231
+ typeof agentPluginCredentialSubjectSchema
232
+ >;
127
233
 
128
- export interface DispatchOptions {
129
- credentialSubject?: AgentPluginCredentialSubject;
130
- destination: {
131
- platform: "slack";
132
- teamId: string;
133
- channelId: string;
134
- };
135
- idempotencyKey: string;
136
- input: string;
137
- metadata?: Record<string, string>;
138
- }
234
+ export type Destination = z.output<typeof destinationSchema>;
235
+
236
+ export type DispatchOptions = z.output<typeof dispatchOptionsSchema>;
139
237
 
140
238
  export interface DispatchResult {
141
239
  id: string;
@@ -256,9 +354,221 @@ export interface SlackConversationLinkHookContext extends AgentPluginContext {
256
354
  conversationId: string;
257
355
  }
258
356
 
357
+ const agentPluginProviderNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
358
+ const agentPluginGrantNameSchema = z.string().regex(/^[a-z][a-z0-9.-]*$/);
359
+ const agentPluginGrantAccessSchema = z.union([
360
+ z.literal("read"),
361
+ z.literal("write"),
362
+ ]);
363
+
364
+ /** Runtime schema for provider authorization a plugin may request. */
365
+ export const agentPluginAuthorizationSchema = z
366
+ .object({
367
+ provider: agentPluginProviderNameSchema,
368
+ scope: nonBlankStringSchema.optional(),
369
+ type: z.literal("oauth"),
370
+ })
371
+ .strict();
372
+
373
+ /** Runtime schema for a provider account attached to stored OAuth tokens. */
374
+ export const agentPluginProviderAccountSchema = z
375
+ .object({
376
+ id: nonBlankStringSchema,
377
+ label: nonBlankStringSchema.optional(),
378
+ url: nonBlankStringSchema.optional(),
379
+ })
380
+ .strict();
381
+
382
+ /** Runtime schema for a plugin-defined outbound credential grant. */
383
+ export const agentPluginGrantSchema = z
384
+ .object({
385
+ access: agentPluginGrantAccessSchema,
386
+ name: agentPluginGrantNameSchema,
387
+ reason: nonBlankStringSchema.optional(),
388
+ requirements: z.array(nonBlankStringSchema).min(1).optional(),
389
+ })
390
+ .strict();
391
+
392
+ /** Runtime schema for plugin-issued header mutations. */
393
+ export const agentPluginCredentialHeaderTransformSchema = z
394
+ .object({
395
+ domain: z.string().min(1),
396
+ headers: z
397
+ .record(z.string(), z.string())
398
+ .refine((headers) => Object.keys(headers).length > 0),
399
+ })
400
+ .strict();
401
+
402
+ /** Runtime schema for a short-lived plugin-issued credential lease. */
403
+ export const agentPluginCredentialLeaseSchema = z
404
+ .object({
405
+ account: agentPluginProviderAccountSchema.optional(),
406
+ authorization: agentPluginAuthorizationSchema.optional(),
407
+ expiresAt: z.string().refine((value) => Number.isFinite(Date.parse(value))),
408
+ headerTransforms: z
409
+ .array(agentPluginCredentialHeaderTransformSchema)
410
+ .min(1),
411
+ })
412
+ .strict();
413
+
414
+ /** Runtime schema for the result returned by a plugin credential hook. */
415
+ export const agentPluginCredentialResultSchema = z.discriminatedUnion("type", [
416
+ z
417
+ .object({
418
+ lease: agentPluginCredentialLeaseSchema,
419
+ type: z.literal("lease"),
420
+ })
421
+ .strict(),
422
+ z
423
+ .object({
424
+ authorization: agentPluginAuthorizationSchema.optional(),
425
+ message: nonBlankStringSchema,
426
+ type: z.literal("needed"),
427
+ })
428
+ .strict(),
429
+ z
430
+ .object({
431
+ message: nonBlankStringSchema,
432
+ type: z.literal("unavailable"),
433
+ })
434
+ .strict(),
435
+ ]);
436
+
437
+ export type AgentPluginGrantAccess = z.output<
438
+ typeof agentPluginGrantAccessSchema
439
+ >;
440
+
441
+ /** Provider authorization Junior can start when a plugin-owned grant is missing. */
442
+ export type AgentPluginAuthorization = z.output<
443
+ typeof agentPluginAuthorizationSchema
444
+ >;
445
+
446
+ /** Interrupt sandbox egress so Junior can start provider authorization. */
447
+ export class EgressAuthRequired extends Error {
448
+ authorization?: AgentPluginAuthorization;
449
+
450
+ constructor(
451
+ message: string,
452
+ options?: {
453
+ authorization?: AgentPluginAuthorization;
454
+ cause?: unknown;
455
+ },
456
+ ) {
457
+ super(message, { cause: options?.cause });
458
+ this.name = "EgressAuthRequired";
459
+ this.authorization = options?.authorization;
460
+ }
461
+ }
462
+
463
+ /** Provider account identity resolved by a plugin OAuth hook. */
464
+ export type AgentPluginProviderAccount = z.output<
465
+ typeof agentPluginProviderAccountSchema
466
+ >;
467
+
468
+ /** Plugin-defined grant required before Junior can forward one outbound request. */
469
+ export type AgentPluginGrant = z.output<typeof agentPluginGrantSchema>;
470
+
471
+ /** Request details available while selecting the grant for sandbox egress. */
472
+ export interface AgentPluginEgressRequest {
473
+ /** Capped request body text when the host exposes it for provider-specific grant classification. */
474
+ bodyText?: string;
475
+ method: string;
476
+ url: string;
477
+ }
478
+
479
+ export interface EgressHookContext extends AgentPluginContext {
480
+ request: AgentPluginEgressRequest;
481
+ }
482
+
483
+ export interface AgentPluginEgressResponse {
484
+ /** Snapshot of upstream response headers; mutations do not affect pass-through. */
485
+ headers: Headers;
486
+ readText(maxBytes: number): Promise<string | undefined>;
487
+ status: number;
488
+ }
489
+
490
+ export interface EgressResponseHookContext extends AgentPluginContext {
491
+ grant: AgentPluginGrant;
492
+ permissionDenied(message: string): void;
493
+ request: Omit<AgentPluginEgressRequest, "bodyText">;
494
+ response: AgentPluginEgressResponse;
495
+ }
496
+
497
+ /** Header mutations a plugin-issued credential lease may apply to owned domains. */
498
+ export type AgentPluginCredentialHeaderTransform = z.output<
499
+ typeof agentPluginCredentialHeaderTransformSchema
500
+ >;
501
+
502
+ /** Short-lived credential headers issued by a plugin for a selected grant. */
503
+ export type AgentPluginCredentialLease = z.output<
504
+ typeof agentPluginCredentialLeaseSchema
505
+ >;
506
+
507
+ export type AgentPluginCredentialResult = z.output<
508
+ typeof agentPluginCredentialResultSchema
509
+ >;
510
+
511
+ export type AgentPluginCredentialActor =
512
+ | {
513
+ type: "system";
514
+ id: string;
515
+ }
516
+ | {
517
+ type: "user";
518
+ userId: string;
519
+ };
520
+
521
+ export interface AgentPluginResolvedCredentialUser {
522
+ type: "user";
523
+ userId: string;
524
+ }
525
+
526
+ export interface AgentPluginStoredTokens {
527
+ account?: AgentPluginProviderAccount;
528
+ accessToken: string;
529
+ expiresAt?: number;
530
+ refreshToken: string;
531
+ scope?: string;
532
+ }
533
+
534
+ export interface AgentPluginUserTokenSlot {
535
+ get(): Promise<AgentPluginStoredTokens | undefined>;
536
+ set(tokens: AgentPluginStoredTokens): Promise<void>;
537
+ userId: string;
538
+ }
539
+
540
+ export interface AgentPluginTokenStore {
541
+ credentialSubject?: AgentPluginUserTokenSlot;
542
+ currentUser?: AgentPluginUserTokenSlot;
543
+ }
544
+
545
+ export interface ResolveOAuthAccountHookContext extends AgentPluginContext {
546
+ tokens: AgentPluginStoredTokens;
547
+ }
548
+
549
+ export interface IssueCredentialHookContext extends AgentPluginContext {
550
+ actor: AgentPluginCredentialActor;
551
+ credentialSubject?: AgentPluginResolvedCredentialUser;
552
+ grant: AgentPluginGrant;
553
+ tokens: AgentPluginTokenStore;
554
+ }
555
+
259
556
  export interface AgentPluginHooks {
260
557
  sandboxPrepare?(ctx: SandboxPrepareHookContext): Promise<void> | void;
261
558
  beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
559
+ grantForEgress?(
560
+ ctx: EgressHookContext,
561
+ ): Promise<AgentPluginGrant | undefined> | AgentPluginGrant | undefined;
562
+ issueCredential?(
563
+ ctx: IssueCredentialHookContext,
564
+ ): Promise<AgentPluginCredentialResult> | AgentPluginCredentialResult;
565
+ onEgressResponse?(ctx: EgressResponseHookContext): Promise<void> | void;
566
+ resolveOAuthAccount?(
567
+ ctx: ResolveOAuthAccountHookContext,
568
+ ):
569
+ | Promise<AgentPluginProviderAccount | undefined>
570
+ | AgentPluginProviderAccount
571
+ | undefined;
262
572
  routes?(ctx: RouteRegistrationHookContext): AgentPluginRoute[];
263
573
  tools?(
264
574
  ctx: ToolRegistrationHookContext,
@@ -283,6 +593,21 @@ export interface JuniorPluginOAuthConfig {
283
593
  clientIdEnv: string;
284
594
  clientSecretEnv: string;
285
595
  scope?: string;
596
+ /**
597
+ * Treat a provider token response with `scope: ""` like an omitted scope and
598
+ * fall back to the requested scope string when storing the token.
599
+ *
600
+ * Enable this only for providers whose token responses cannot report OAuth
601
+ * scopes even though Junior needs a local requested-scope string for
602
+ * reauthorization checks. The built-in GitHub App plugin enables this because
603
+ * GitHub App user-to-server tokens always return an empty scope value — their
604
+ * effective access is enforced by GitHub App permissions, installation
605
+ * repository access, and the requesting user's own access, not OAuth scopes.
606
+ *
607
+ * Do not enable this for standard OAuth providers where an explicit empty
608
+ * `scope` means the provider granted no scopes.
609
+ */
610
+ treatEmptyScopeAsUnreported?: boolean;
286
611
  tokenAuthMethod?: "body" | "basic";
287
612
  tokenEndpoint: string;
288
613
  tokenExtraHeaders?: Record<string, string>;
@@ -296,20 +621,7 @@ export interface JuniorPluginOAuthBearerCredentials {
296
621
  type: "oauth-bearer";
297
622
  }
298
623
 
299
- export interface JuniorPluginGitHubAppCredentials {
300
- apiHeaders?: Record<string, string>;
301
- appIdEnv: string;
302
- authTokenEnv: string;
303
- authTokenPlaceholder?: string;
304
- domains: string[];
305
- installationIdEnv: string;
306
- privateKeyEnv: string;
307
- type: "github-app";
308
- }
309
-
310
- export type JuniorPluginCredentials =
311
- | JuniorPluginOAuthBearerCredentials
312
- | JuniorPluginGitHubAppCredentials;
624
+ export type JuniorPluginCredentials = JuniorPluginOAuthBearerCredentials;
313
625
 
314
626
  export interface JuniorPluginNpmRuntimeDependency {
315
627
  package: string;
@@ -392,7 +704,7 @@ export function defineJuniorPlugin(
392
704
  ): JuniorPluginRegistration {
393
705
  if ("pluginConfig" in plugin) {
394
706
  throw new Error(
395
- "pluginConfig is no longer supported. Put runtime metadata in manifest and trusted state prefixes on the plugin registration.",
707
+ "pluginConfig is no longer supported. Put runtime metadata in manifest and state prefixes on the plugin registration.",
396
708
  );
397
709
  }
398
710
  const manifest = plugin.manifest;