@sentry/junior-plugin-api 0.68.0 → 0.69.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,143 @@ 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
+ /** Provider account identity resolved by a plugin OAuth hook. */
325
+ export type AgentPluginProviderAccount = z.output<typeof agentPluginProviderAccountSchema>;
326
+ /** Plugin-defined grant required before Junior can forward one outbound request. */
327
+ export type AgentPluginGrant = z.output<typeof agentPluginGrantSchema>;
328
+ /** Request details available while selecting the grant for sandbox egress. */
329
+ export interface AgentPluginEgressRequest {
330
+ method: string;
331
+ url: string;
332
+ }
333
+ export interface EgressHookContext extends AgentPluginContext {
334
+ request: AgentPluginEgressRequest;
335
+ }
336
+ /** Header mutations a plugin-issued credential lease may apply to owned domains. */
337
+ export type AgentPluginCredentialHeaderTransform = z.output<typeof agentPluginCredentialHeaderTransformSchema>;
338
+ /** Short-lived credential headers issued by a plugin for a selected grant. */
339
+ export type AgentPluginCredentialLease = z.output<typeof agentPluginCredentialLeaseSchema>;
340
+ export type AgentPluginCredentialResult = z.output<typeof agentPluginCredentialResultSchema>;
341
+ export type AgentPluginCredentialActor = {
342
+ type: "system";
343
+ id: string;
344
+ } | {
345
+ type: "user";
346
+ userId: string;
347
+ };
348
+ export interface AgentPluginResolvedCredentialUser {
349
+ type: "user";
350
+ userId: string;
351
+ }
352
+ export interface AgentPluginStoredTokens {
353
+ account?: AgentPluginProviderAccount;
354
+ accessToken: string;
355
+ expiresAt?: number;
356
+ refreshToken: string;
357
+ scope?: string;
358
+ }
359
+ export interface AgentPluginUserTokenSlot {
360
+ get(): Promise<AgentPluginStoredTokens | undefined>;
361
+ set(tokens: AgentPluginStoredTokens): Promise<void>;
362
+ userId: string;
363
+ }
364
+ export interface AgentPluginTokenStore {
365
+ credentialSubject?: AgentPluginUserTokenSlot;
366
+ currentUser?: AgentPluginUserTokenSlot;
367
+ }
368
+ export interface ResolveOAuthAccountHookContext extends AgentPluginContext {
369
+ tokens: AgentPluginStoredTokens;
370
+ }
371
+ export interface IssueCredentialHookContext extends AgentPluginContext {
372
+ actor: AgentPluginCredentialActor;
373
+ credentialSubject?: AgentPluginResolvedCredentialUser;
374
+ grant: AgentPluginGrant;
375
+ tokens: AgentPluginTokenStore;
376
+ }
205
377
  export interface AgentPluginHooks {
206
378
  sandboxPrepare?(ctx: SandboxPrepareHookContext): Promise<void> | void;
207
379
  beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
380
+ grantForEgress?(ctx: EgressHookContext): Promise<AgentPluginGrant | undefined> | AgentPluginGrant | undefined;
381
+ issueCredential?(ctx: IssueCredentialHookContext): Promise<AgentPluginCredentialResult> | AgentPluginCredentialResult;
382
+ resolveOAuthAccount?(ctx: ResolveOAuthAccountHookContext): Promise<AgentPluginProviderAccount | undefined> | AgentPluginProviderAccount | undefined;
208
383
  routes?(ctx: RouteRegistrationHookContext): AgentPluginRoute[];
209
384
  tools?(ctx: ToolRegistrationHookContext): Record<string, AgentPluginToolDefinition>;
210
385
  heartbeat?(ctx: HeartbeatHookContext): Promise<HeartbeatResult | void> | HeartbeatResult | void;
@@ -217,6 +392,21 @@ export interface JuniorPluginOAuthConfig {
217
392
  clientIdEnv: string;
218
393
  clientSecretEnv: string;
219
394
  scope?: string;
395
+ /**
396
+ * Treat a provider token response with `scope: ""` like an omitted scope and
397
+ * fall back to the requested scope string when storing the token.
398
+ *
399
+ * Enable this only for providers whose token responses cannot report OAuth
400
+ * scopes even though Junior needs a local requested-scope string for
401
+ * reauthorization checks. The built-in GitHub App plugin enables this because
402
+ * GitHub App user-to-server tokens always return an empty scope value — their
403
+ * effective access is enforced by GitHub App permissions, installation
404
+ * repository access, and the requesting user's own access, not OAuth scopes.
405
+ *
406
+ * Do not enable this for standard OAuth providers where an explicit empty
407
+ * `scope` means the provider granted no scopes.
408
+ */
409
+ treatEmptyScopeAsUnreported?: boolean;
220
410
  tokenAuthMethod?: "body" | "basic";
221
411
  tokenEndpoint: string;
222
412
  tokenExtraHeaders?: Record<string, string>;
@@ -228,17 +418,7 @@ export interface JuniorPluginOAuthBearerCredentials {
228
418
  domains: string[];
229
419
  type: "oauth-bearer";
230
420
  }
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;
421
+ export type JuniorPluginCredentials = JuniorPluginOAuthBearerCredentials;
242
422
  export interface JuniorPluginNpmRuntimeDependency {
243
423
  package: string;
244
424
  type: "npm";
@@ -301,3 +481,4 @@ export interface JuniorPluginRegistration extends JuniorPluginRegistrationInput
301
481
  }
302
482
  /** Define one Junior plugin registration for app and build-time wiring. */
303
483
  export declare function defineJuniorPlugin(plugin: JuniorPluginRegistrationInput): JuniorPluginRegistration;
484
+ export {};
package/dist/index.js CHANGED
@@ -1,15 +1,126 @@
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
+ ]);
8
119
  var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
9
120
  function defineJuniorPlugin(plugin) {
10
121
  if ("pluginConfig" in plugin) {
11
122
  throw new Error(
12
- "pluginConfig is no longer supported. Put runtime metadata in manifest and trusted state prefixes on the plugin registration."
123
+ "pluginConfig is no longer supported. Put runtime metadata in manifest and state prefixes on the plugin registration."
13
124
  );
14
125
  }
15
126
  const manifest = plugin.manifest;
@@ -46,5 +157,15 @@ function defineJuniorPlugin(plugin) {
46
157
  }
47
158
  export {
48
159
  AgentPluginToolInputError,
49
- defineJuniorPlugin
160
+ agentPluginAuthorizationSchema,
161
+ agentPluginCredentialHeaderTransformSchema,
162
+ agentPluginCredentialLeaseSchema,
163
+ agentPluginCredentialResultSchema,
164
+ agentPluginCredentialSubjectSchema,
165
+ agentPluginGrantSchema,
166
+ agentPluginProviderAccountSchema,
167
+ agentPluginRequesterSchema,
168
+ defineJuniorPlugin,
169
+ destinationSchema,
170
+ dispatchOptionsSchema
50
171
  };
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.69.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,187 @@ 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
+ /** Provider account identity resolved by a plugin OAuth hook. */
447
+ export type AgentPluginProviderAccount = z.output<
448
+ typeof agentPluginProviderAccountSchema
449
+ >;
450
+
451
+ /** Plugin-defined grant required before Junior can forward one outbound request. */
452
+ export type AgentPluginGrant = z.output<typeof agentPluginGrantSchema>;
453
+
454
+ /** Request details available while selecting the grant for sandbox egress. */
455
+ export interface AgentPluginEgressRequest {
456
+ method: string;
457
+ url: string;
458
+ }
459
+
460
+ export interface EgressHookContext extends AgentPluginContext {
461
+ request: AgentPluginEgressRequest;
462
+ }
463
+
464
+ /** Header mutations a plugin-issued credential lease may apply to owned domains. */
465
+ export type AgentPluginCredentialHeaderTransform = z.output<
466
+ typeof agentPluginCredentialHeaderTransformSchema
467
+ >;
468
+
469
+ /** Short-lived credential headers issued by a plugin for a selected grant. */
470
+ export type AgentPluginCredentialLease = z.output<
471
+ typeof agentPluginCredentialLeaseSchema
472
+ >;
473
+
474
+ export type AgentPluginCredentialResult = z.output<
475
+ typeof agentPluginCredentialResultSchema
476
+ >;
477
+
478
+ export type AgentPluginCredentialActor =
479
+ | {
480
+ type: "system";
481
+ id: string;
482
+ }
483
+ | {
484
+ type: "user";
485
+ userId: string;
486
+ };
487
+
488
+ export interface AgentPluginResolvedCredentialUser {
489
+ type: "user";
490
+ userId: string;
491
+ }
492
+
493
+ export interface AgentPluginStoredTokens {
494
+ account?: AgentPluginProviderAccount;
495
+ accessToken: string;
496
+ expiresAt?: number;
497
+ refreshToken: string;
498
+ scope?: string;
499
+ }
500
+
501
+ export interface AgentPluginUserTokenSlot {
502
+ get(): Promise<AgentPluginStoredTokens | undefined>;
503
+ set(tokens: AgentPluginStoredTokens): Promise<void>;
504
+ userId: string;
505
+ }
506
+
507
+ export interface AgentPluginTokenStore {
508
+ credentialSubject?: AgentPluginUserTokenSlot;
509
+ currentUser?: AgentPluginUserTokenSlot;
510
+ }
511
+
512
+ export interface ResolveOAuthAccountHookContext extends AgentPluginContext {
513
+ tokens: AgentPluginStoredTokens;
514
+ }
515
+
516
+ export interface IssueCredentialHookContext extends AgentPluginContext {
517
+ actor: AgentPluginCredentialActor;
518
+ credentialSubject?: AgentPluginResolvedCredentialUser;
519
+ grant: AgentPluginGrant;
520
+ tokens: AgentPluginTokenStore;
521
+ }
522
+
259
523
  export interface AgentPluginHooks {
260
524
  sandboxPrepare?(ctx: SandboxPrepareHookContext): Promise<void> | void;
261
525
  beforeToolExecute?(ctx: BeforeToolExecuteHookContext): Promise<void> | void;
526
+ grantForEgress?(
527
+ ctx: EgressHookContext,
528
+ ): Promise<AgentPluginGrant | undefined> | AgentPluginGrant | undefined;
529
+ issueCredential?(
530
+ ctx: IssueCredentialHookContext,
531
+ ): Promise<AgentPluginCredentialResult> | AgentPluginCredentialResult;
532
+ resolveOAuthAccount?(
533
+ ctx: ResolveOAuthAccountHookContext,
534
+ ):
535
+ | Promise<AgentPluginProviderAccount | undefined>
536
+ | AgentPluginProviderAccount
537
+ | undefined;
262
538
  routes?(ctx: RouteRegistrationHookContext): AgentPluginRoute[];
263
539
  tools?(
264
540
  ctx: ToolRegistrationHookContext,
@@ -283,6 +559,21 @@ export interface JuniorPluginOAuthConfig {
283
559
  clientIdEnv: string;
284
560
  clientSecretEnv: string;
285
561
  scope?: string;
562
+ /**
563
+ * Treat a provider token response with `scope: ""` like an omitted scope and
564
+ * fall back to the requested scope string when storing the token.
565
+ *
566
+ * Enable this only for providers whose token responses cannot report OAuth
567
+ * scopes even though Junior needs a local requested-scope string for
568
+ * reauthorization checks. The built-in GitHub App plugin enables this because
569
+ * GitHub App user-to-server tokens always return an empty scope value — their
570
+ * effective access is enforced by GitHub App permissions, installation
571
+ * repository access, and the requesting user's own access, not OAuth scopes.
572
+ *
573
+ * Do not enable this for standard OAuth providers where an explicit empty
574
+ * `scope` means the provider granted no scopes.
575
+ */
576
+ treatEmptyScopeAsUnreported?: boolean;
286
577
  tokenAuthMethod?: "body" | "basic";
287
578
  tokenEndpoint: string;
288
579
  tokenExtraHeaders?: Record<string, string>;
@@ -296,20 +587,7 @@ export interface JuniorPluginOAuthBearerCredentials {
296
587
  type: "oauth-bearer";
297
588
  }
298
589
 
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;
590
+ export type JuniorPluginCredentials = JuniorPluginOAuthBearerCredentials;
313
591
 
314
592
  export interface JuniorPluginNpmRuntimeDependency {
315
593
  package: string;
@@ -392,7 +670,7 @@ export function defineJuniorPlugin(
392
670
  ): JuniorPluginRegistration {
393
671
  if ("pluginConfig" in plugin) {
394
672
  throw new Error(
395
- "pluginConfig is no longer supported. Put runtime metadata in manifest and trusted state prefixes on the plugin registration.",
673
+ "pluginConfig is no longer supported. Put runtime metadata in manifest and state prefixes on the plugin registration.",
396
674
  );
397
675
  }
398
676
  const manifest = plugin.manifest;