@sentry/junior 0.74.1 → 0.76.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.
Files changed (121) hide show
  1. package/README.md +1 -1
  2. package/bin/junior.mjs +4 -66
  3. package/dist/agent-hooks-ZOE7RIED.js +37 -0
  4. package/dist/api-reference.d.ts +3 -1
  5. package/dist/app.js +5516 -5422
  6. package/dist/build/copy-build-content.d.ts +1 -1
  7. package/dist/build/virtual-config.d.ts +2 -2
  8. package/dist/chat/agent-dispatch/context.d.ts +2 -3
  9. package/dist/chat/agent-dispatch/runner.d.ts +2 -0
  10. package/dist/chat/agent-dispatch/types.d.ts +2 -1
  11. package/dist/chat/config.d.ts +3 -0
  12. package/dist/chat/credentials/state-adapter-token-store.d.ts +2 -0
  13. package/dist/chat/credentials/subject.d.ts +3 -3
  14. package/dist/chat/credentials/user-token-store.d.ts +17 -12
  15. package/dist/chat/db.d.ts +8 -0
  16. package/dist/chat/mcp/auth-store.d.ts +2 -1
  17. package/dist/chat/mcp/oauth.d.ts +2 -1
  18. package/dist/chat/oauth-flow.d.ts +3 -1
  19. package/dist/chat/pi/client.d.ts +15 -7
  20. package/dist/chat/plugins/agent-hooks.d.ts +20 -13
  21. package/dist/chat/plugins/auth/oauth-request.d.ts +11 -7
  22. package/dist/chat/plugins/credential-hooks.d.ts +6 -6
  23. package/dist/chat/plugins/logging.d.ts +2 -2
  24. package/dist/chat/plugins/model.d.ts +9 -0
  25. package/dist/chat/plugins/package-discovery.d.ts +2 -1
  26. package/dist/chat/plugins/prompt.d.ts +5 -0
  27. package/dist/chat/plugins/registry.d.ts +4 -0
  28. package/dist/chat/plugins/state.d.ts +3 -5
  29. package/dist/chat/plugins/task-callback.d.ts +5 -0
  30. package/dist/chat/plugins/task-message.d.ts +23 -0
  31. package/dist/chat/plugins/task-queue.d.ts +5 -0
  32. package/dist/chat/plugins/task-runner.d.ts +12 -0
  33. package/dist/chat/plugins/task-signing.d.ts +31 -0
  34. package/dist/chat/plugins/types.d.ts +1 -0
  35. package/dist/chat/plugins/validation.d.ts +5 -0
  36. package/dist/chat/prompt.d.ts +15 -1
  37. package/dist/chat/requester.d.ts +6 -5
  38. package/dist/chat/respond-helpers.d.ts +2 -0
  39. package/dist/chat/respond.d.ts +13 -2
  40. package/dist/chat/runtime/agent-continue-runner.d.ts +4 -0
  41. package/dist/chat/runtime/reply-executor.d.ts +5 -1
  42. package/dist/chat/runtime/slack-resume.d.ts +10 -2
  43. package/dist/chat/runtime/slack-runtime.d.ts +6 -1
  44. package/dist/chat/sandbox/egress-credentials.d.ts +8 -8
  45. package/dist/chat/sandbox/sandbox.d.ts +2 -2
  46. package/dist/chat/sentry.d.ts +1 -0
  47. package/dist/chat/services/mcp-auth-orchestration.d.ts +2 -1
  48. package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -1
  49. package/dist/chat/services/subscribed-decision.d.ts +2 -2
  50. package/dist/chat/services/turn-session-record.d.ts +11 -7
  51. package/dist/chat/sql/db.d.ts +3 -0
  52. package/dist/chat/sql/executor.d.ts +7 -0
  53. package/dist/chat/sql/neon.d.ts +2 -4
  54. package/dist/chat/sql/postgres.d.ts +6 -0
  55. package/dist/chat/state/turn-session.d.ts +8 -5
  56. package/dist/chat/task-execution/state.d.ts +7 -2
  57. package/dist/chat/task-execution/worker.d.ts +1 -1
  58. package/dist/chat/tools/agent-tools.d.ts +9 -2
  59. package/dist/chat/tools/slack/context.d.ts +2 -2
  60. package/dist/chat/tools/types.d.ts +7 -4
  61. package/dist/chat/vercel-queue-client.d.ts +3 -0
  62. package/dist/{chunk-YOHFWWBV.js → chunk-2ECJXSVQ.js} +5 -107
  63. package/dist/{chunk-OR6NQJ5E.js → chunk-4SCWV7TJ.js} +3 -3
  64. package/dist/chunk-4UO6FK4G.js +64 -0
  65. package/dist/chunk-56TBVRJG.js +115 -0
  66. package/dist/{chunk-3BYAPS6B.js → chunk-EJN6G5A2.js} +17 -11
  67. package/dist/{chunk-SQGMG7OD.js → chunk-HHDUKWVG.js} +508 -149
  68. package/dist/{chunk-6UP2Z2RZ.js → chunk-JBASI5VV.js} +7 -7
  69. package/dist/chunk-KNFROR7R.js +127 -0
  70. package/dist/{chunk-HYHKTFG2.js → chunk-KOIMO7S3.js} +186 -910
  71. package/dist/chunk-MLKGABMK.js +9 -0
  72. package/dist/chunk-NFTMTIP3.js +964 -0
  73. package/dist/chunk-NYKJ3KON.js +1082 -0
  74. package/dist/{chunk-SJHUF3DP.js → chunk-OJ53FYVG.js} +2 -10
  75. package/dist/{chunk-KVZL5NZS.js → chunk-Q3XNY442.js} +17 -7
  76. package/dist/{chunk-YRDS7VKO.js → chunk-Q6XFTRV5.js} +2 -2
  77. package/dist/chunk-R6Z5XWY3.js +1076 -0
  78. package/dist/chunk-RV5RYIJW.js +56 -0
  79. package/dist/chunk-SG5WAA7H.js +132 -0
  80. package/dist/chunk-ST6YNAXG.js +54 -0
  81. package/dist/{chunk-GM7HTXYC.js → chunk-T77LUIX3.js} +148 -151
  82. package/dist/{chunk-CYUI7JU5.js → chunk-VALUBQ7R.js} +22 -30
  83. package/dist/chunk-XBBC6W45.js +71 -0
  84. package/dist/chunk-Y2CM7HXH.js +111 -0
  85. package/dist/{chunk-F6HWCPOC.js → chunk-Y5OFBCBZ.js} +1 -1
  86. package/dist/{chunk-M4FLLXXD.js → chunk-Z4CIQ3EB.js} +5 -1
  87. package/dist/{chunk-7Q5YOUUT.js → chunk-ZLMBNBUG.js} +146 -52
  88. package/dist/{chunk-2LUZA3LY.js → chunk-ZQB37HUX.js} +11 -11
  89. package/dist/cli/chat.js +87 -8
  90. package/dist/cli/check.js +8 -7
  91. package/dist/cli/env.js +4 -53
  92. package/dist/cli/init.js +6 -1
  93. package/dist/cli/main.js +84 -0
  94. package/dist/cli/plugins.js +244 -0
  95. package/dist/cli/run.js +5 -52
  96. package/dist/cli/snapshot-warmup.js +12 -11
  97. package/dist/cli/upgrade.js +385 -26
  98. package/dist/db-7A7PFRGL.js +17 -0
  99. package/dist/deployment.d.ts +1 -0
  100. package/dist/handlers/sandbox-egress-route.d.ts +4 -0
  101. package/dist/handlers/slack-webhook.d.ts +4 -0
  102. package/dist/handlers/webhooks.d.ts +6 -13
  103. package/dist/instrumentation.js +14 -18
  104. package/dist/nitro.d.ts +1 -1
  105. package/dist/nitro.js +67 -101
  106. package/dist/plugin-module.d.ts +21 -0
  107. package/dist/plugins-PZMDS7AT.js +15 -0
  108. package/dist/plugins.d.ts +9 -5
  109. package/dist/registry-OIPAJU2O.js +46 -0
  110. package/dist/reporting/conversations.d.ts +3 -3
  111. package/dist/reporting.d.ts +6 -5
  112. package/dist/reporting.js +42 -28
  113. package/dist/{runner-27NP2TEO.js → runner-KPLNHDCV.js} +77 -19
  114. package/dist/sentry-4CP5NNQ5.js +31 -0
  115. package/dist/validation-SLA6IGF7.js +15 -0
  116. package/dist/vercel.js +1 -1
  117. package/package.json +14 -11
  118. package/dist/chat/conversations/configured.d.ts +0 -5
  119. package/dist/chat/conversations/state.d.ts +0 -4
  120. package/dist/chunk-2KG3PWR4.js +0 -17
  121. package/dist/chunk-JL2SLRAT.js +0 -1970
@@ -0,0 +1,964 @@
1
+ import {
2
+ createPluginLogger,
3
+ createPluginState
4
+ } from "./chunk-56TBVRJG.js";
5
+ import {
6
+ getDb
7
+ } from "./chunk-NYKJ3KON.js";
8
+ import {
9
+ SANDBOX_WORKSPACE_ROOT
10
+ } from "./chunk-G3E7SCME.js";
11
+ import {
12
+ isConversationChannel,
13
+ isConversationScopedChannel,
14
+ isDmChannel,
15
+ normalizeSlackConversationId
16
+ } from "./chunk-Q6XFTRV5.js";
17
+ import {
18
+ botConfig,
19
+ completeObject,
20
+ embedTexts
21
+ } from "./chunk-T77LUIX3.js";
22
+ import {
23
+ isActorUserId,
24
+ parseActorUserId
25
+ } from "./chunk-VALUBQ7R.js";
26
+ import {
27
+ logInfo,
28
+ logWarn
29
+ } from "./chunk-EJN6G5A2.js";
30
+
31
+ // src/chat/plugins/agent-hooks.ts
32
+ import { promptMessageSchema } from "@sentry/junior-plugin-api";
33
+
34
+ // src/chat/plugins/model.ts
35
+ function createPluginModel(pluginName, options = {}, runtime = {}) {
36
+ return {
37
+ async completeObject(input) {
38
+ const modelId = options.structuredModelId ?? (options.structuredModel === "default" ? botConfig.modelId : botConfig.fastModelId);
39
+ const result = await completeObject({
40
+ modelId,
41
+ schema: input.schema,
42
+ prompt: input.prompt,
43
+ ...input.system !== void 0 ? { system: input.system } : {},
44
+ ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : {},
45
+ signal: runtime.signal,
46
+ metadata: {
47
+ pluginName,
48
+ pluginModelRole: "structured"
49
+ }
50
+ });
51
+ return { object: result.object };
52
+ }
53
+ };
54
+ }
55
+ function createPluginEmbedder(pluginName, runtime = {}) {
56
+ return {
57
+ async embedTexts(input) {
58
+ return await embedTexts({
59
+ modelId: botConfig.embeddingModelId,
60
+ texts: input.texts,
61
+ signal: runtime.signal,
62
+ metadata: {
63
+ pluginName,
64
+ pluginModelRole: "embedding"
65
+ }
66
+ });
67
+ }
68
+ };
69
+ }
70
+
71
+ // src/chat/tools/slack/context.ts
72
+ function getSlackToolContext(context) {
73
+ if (context.source.platform !== "slack") {
74
+ return void 0;
75
+ }
76
+ if (context.destination.platform !== "slack") {
77
+ throw new TypeError("Slack source requires a Slack destination");
78
+ }
79
+ return {
80
+ destination: context.destination,
81
+ source: context.source,
82
+ requester: context.requester?.platform === "slack" ? context.requester : void 0,
83
+ destinationChannelId: context.destination.channelId,
84
+ messageTs: context.source.messageTs,
85
+ sourceChannelId: context.source.channelId,
86
+ teamId: context.source.teamId,
87
+ threadTs: context.source.threadTs
88
+ };
89
+ }
90
+
91
+ // src/chat/credentials/subject.ts
92
+ import { createHmac, timingSafeEqual } from "crypto";
93
+ var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
94
+ var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
95
+ function getCredentialSubjectSecret() {
96
+ return process.env.JUNIOR_SECRET?.trim() || void 0;
97
+ }
98
+ function buildPayload(input) {
99
+ return [
100
+ CREDENTIAL_SUBJECT_HMAC_CONTEXT,
101
+ input.allowedWhen,
102
+ input.teamId,
103
+ input.channelId,
104
+ input.userId
105
+ ].join("\0");
106
+ }
107
+ function signPayload(secret, payload) {
108
+ const digest = createHmac("sha256", secret).update(payload).digest("hex");
109
+ return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
110
+ }
111
+ function timingSafeMatch(expected, actual) {
112
+ const expectedBuffer = Buffer.from(expected);
113
+ const actualBuffer = Buffer.from(actual);
114
+ if (expectedBuffer.length !== actualBuffer.length) {
115
+ return false;
116
+ }
117
+ return timingSafeEqual(expectedBuffer, actualBuffer);
118
+ }
119
+ function createSlackDirectCredentialSubject(input) {
120
+ const channelId = normalizeSlackConversationId(input.channelId);
121
+ const teamId = input.teamId?.trim();
122
+ const userId = parseActorUserId(input.userId);
123
+ if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
124
+ return void 0;
125
+ }
126
+ return {
127
+ type: "user",
128
+ userId,
129
+ allowedWhen: "private-direct-conversation"
130
+ };
131
+ }
132
+ function bindSlackDirectCredentialSubject(input) {
133
+ const channelId = normalizeSlackConversationId(input.channelId);
134
+ const teamId = input.teamId.trim();
135
+ const secret = getCredentialSubjectSecret();
136
+ const { subject } = input;
137
+ const userId = parseActorUserId(subject.userId);
138
+ if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
139
+ return void 0;
140
+ }
141
+ return {
142
+ type: "user",
143
+ userId,
144
+ allowedWhen: subject.allowedWhen,
145
+ binding: {
146
+ type: "slack-direct-conversation",
147
+ teamId,
148
+ channelId,
149
+ signature: signPayload(
150
+ secret,
151
+ buildPayload({
152
+ allowedWhen: subject.allowedWhen,
153
+ teamId,
154
+ channelId,
155
+ userId
156
+ })
157
+ )
158
+ }
159
+ };
160
+ }
161
+ function verifySlackDirectCredentialSubject(input) {
162
+ const channelId = normalizeSlackConversationId(input.channelId);
163
+ const secret = getCredentialSubjectSecret();
164
+ if (!channelId || !secret) {
165
+ return false;
166
+ }
167
+ const { subject } = input;
168
+ const binding = subject.binding;
169
+ if (subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) {
170
+ return false;
171
+ }
172
+ const expected = signPayload(
173
+ secret,
174
+ buildPayload({
175
+ allowedWhen: subject.allowedWhen,
176
+ teamId: binding.teamId,
177
+ channelId: binding.channelId,
178
+ userId: subject.userId
179
+ })
180
+ );
181
+ return timingSafeMatch(expected, binding.signature);
182
+ }
183
+
184
+ // src/chat/tools/channel-capabilities.ts
185
+ function resolveChannelCapabilities(channelId) {
186
+ return {
187
+ canCreateCanvas: isConversationScopedChannel(channelId),
188
+ canPostToChannel: isConversationChannel(channelId),
189
+ canAddReactions: isConversationScopedChannel(channelId)
190
+ };
191
+ }
192
+
193
+ // src/chat/plugins/agent-hooks.ts
194
+ import { z } from "zod";
195
+ var PluginHookDeniedError = class extends Error {
196
+ constructor(message) {
197
+ super(message);
198
+ this.name = "PluginHookDeniedError";
199
+ }
200
+ };
201
+ var registeredPlugins = [];
202
+ var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
203
+ var PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
204
+ var OPERATIONAL_REPORT_MAX_METRICS = 8;
205
+ var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
206
+ var OPERATIONAL_REPORT_MAX_FIELDS = 8;
207
+ var OPERATIONAL_REPORT_MAX_RECORDS = 25;
208
+ var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
209
+ var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
210
+ var PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
211
+ "GET",
212
+ "POST",
213
+ "PUT",
214
+ "PATCH",
215
+ "DELETE",
216
+ "HEAD",
217
+ "OPTIONS",
218
+ "ALL"
219
+ ]);
220
+ var PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS = 16e3;
221
+ var systemPromptMessageArraySchema = z.array(promptMessageSchema);
222
+ var userPromptMessageArraySchema = z.array(promptMessageSchema);
223
+ function isRecord(value) {
224
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
225
+ }
226
+ function basePluginContext(plugin) {
227
+ const name = plugin.manifest.name;
228
+ return {
229
+ plugin: { name },
230
+ log: createPluginLogger(name),
231
+ db: getDb()
232
+ };
233
+ }
234
+ function systemPromptPluginContext(plugin) {
235
+ return {
236
+ ...basePluginContext(plugin)
237
+ };
238
+ }
239
+ function invocationPluginContext(plugin, context) {
240
+ const base = basePluginContext(plugin);
241
+ const common = {
242
+ ...base,
243
+ conversationId: context.conversationId,
244
+ embedder: createPluginEmbedder(plugin.manifest.name),
245
+ source: context.source,
246
+ text: context.userText ?? "",
247
+ state: createPluginState(plugin.manifest.name)
248
+ };
249
+ if (context.source.platform === "slack") {
250
+ if (context.destination.platform !== "slack") {
251
+ throw new TypeError(
252
+ "Slack plugin prompt context requires Slack destination"
253
+ );
254
+ }
255
+ return {
256
+ ...common,
257
+ destination: context.destination,
258
+ requester: context.requester?.platform === "slack" ? context.requester : void 0
259
+ };
260
+ }
261
+ if (context.destination.platform !== "local") {
262
+ throw new TypeError(
263
+ "Local plugin prompt context requires local destination"
264
+ );
265
+ }
266
+ return {
267
+ ...common,
268
+ destination: context.destination,
269
+ requester: context.requester?.platform === "local" ? context.requester : void 0
270
+ };
271
+ }
272
+ function safeErrorMessage(error) {
273
+ return error instanceof Error ? error.message : String(error);
274
+ }
275
+ function toPromptContributionContext(args) {
276
+ return {
277
+ id: `${args.hookName}:${args.index}`,
278
+ pluginName: args.pluginName,
279
+ text: args.message.text
280
+ };
281
+ }
282
+ function logInvalidPromptContributions(args) {
283
+ logWarn(
284
+ "plugin_prompt_contribution_result_invalid",
285
+ {},
286
+ {
287
+ "app.plugin.hook": args.hookName,
288
+ "app.plugin.name": args.pluginName,
289
+ "app.plugin.validation_reason": "invalid_shape"
290
+ },
291
+ "Plugin prompt contribution result invalid"
292
+ );
293
+ }
294
+ function validatePlugins(plugins) {
295
+ const seen = /* @__PURE__ */ new Set();
296
+ for (const plugin of plugins) {
297
+ const name = plugin.manifest.name;
298
+ if (!PLUGIN_NAME_RE.test(name)) {
299
+ throw new Error(
300
+ `Plugin name "${name}" must be a lowercase plugin identifier`
301
+ );
302
+ }
303
+ if (seen.has(name)) {
304
+ throw new Error(`Duplicate plugin name "${name}"`);
305
+ }
306
+ for (const [taskName, task] of Object.entries(plugin.tasks ?? {})) {
307
+ if (!PLUGIN_TOOL_NAME_RE.test(taskName)) {
308
+ throw new Error(
309
+ `Plugin task "${taskName}" from plugin "${name}" must be a camelCase identifier`
310
+ );
311
+ }
312
+ if (typeof task.run !== "function") {
313
+ throw new Error(
314
+ `Plugin task "${taskName}" from plugin "${name}" must define a run function`
315
+ );
316
+ }
317
+ }
318
+ seen.add(name);
319
+ }
320
+ }
321
+ function setPlugins(nextPlugins) {
322
+ validatePlugins(nextPlugins);
323
+ const previous = registeredPlugins;
324
+ registeredPlugins = [...nextPlugins].sort(
325
+ (left, right) => left.manifest.name.localeCompare(right.manifest.name)
326
+ );
327
+ return previous;
328
+ }
329
+ function getPlugins() {
330
+ return [...registeredPlugins];
331
+ }
332
+ async function getPluginSystemPromptContributions(source) {
333
+ const contributions = [];
334
+ let totalChars = 0;
335
+ for (const plugin of getPlugins()) {
336
+ const pluginName = plugin.manifest.name;
337
+ const hook = plugin.hooks?.systemPrompt;
338
+ if (!hook) {
339
+ continue;
340
+ }
341
+ try {
342
+ const pluginContributions = await hook({
343
+ ...systemPromptPluginContext(plugin),
344
+ platform: source.platform
345
+ });
346
+ const result = systemPromptMessageArraySchema.safeParse(pluginContributions);
347
+ if (!result.success) {
348
+ logInvalidPromptContributions({
349
+ hookName: "systemPrompt",
350
+ pluginName
351
+ });
352
+ continue;
353
+ }
354
+ const acceptedContributions = result.data.map(
355
+ (message, index) => toPromptContributionContext({
356
+ hookName: "systemPrompt",
357
+ index,
358
+ message,
359
+ pluginName
360
+ })
361
+ );
362
+ const pluginContributionChars = acceptedContributions.reduce(
363
+ (sum, contribution) => sum + contribution.text.length,
364
+ 0
365
+ );
366
+ if (totalChars + pluginContributionChars > PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS) {
367
+ logWarn(
368
+ "plugin_system_prompt_contribution_budget_exceeded",
369
+ {},
370
+ {
371
+ "app.plugin.name": pluginName
372
+ },
373
+ "Plugin system prompt contributions exceeded budget"
374
+ );
375
+ continue;
376
+ }
377
+ totalChars += pluginContributionChars;
378
+ contributions.push(...acceptedContributions);
379
+ } catch (error) {
380
+ logWarn(
381
+ "plugin_system_prompt_hook_failed",
382
+ {},
383
+ {
384
+ "app.plugin.name": pluginName,
385
+ "exception.message": safeErrorMessage(error)
386
+ },
387
+ "Plugin system prompt hook failed"
388
+ );
389
+ }
390
+ }
391
+ return contributions;
392
+ }
393
+ async function getPluginUserPromptContributions(args) {
394
+ const contributions = [];
395
+ let totalChars = 0;
396
+ for (const plugin of getPlugins()) {
397
+ const pluginName = plugin.manifest.name;
398
+ const hook = plugin.hooks?.userPrompt;
399
+ if (!hook) {
400
+ continue;
401
+ }
402
+ try {
403
+ const rawResult = await hook({
404
+ ...invocationPluginContext(plugin, args.context)
405
+ });
406
+ if (rawResult === void 0) {
407
+ continue;
408
+ }
409
+ const result = userPromptMessageArraySchema.safeParse(rawResult);
410
+ if (!result.success) {
411
+ logInvalidPromptContributions({
412
+ hookName: "userPrompt",
413
+ pluginName
414
+ });
415
+ continue;
416
+ }
417
+ const acceptedContributions = result.data.map(
418
+ (message, index) => toPromptContributionContext({
419
+ hookName: "userPrompt",
420
+ index,
421
+ message,
422
+ pluginName
423
+ })
424
+ );
425
+ const pluginContributionChars = acceptedContributions.reduce(
426
+ (sum, contribution) => sum + contribution.text.length,
427
+ 0
428
+ );
429
+ if (totalChars + pluginContributionChars > PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS) {
430
+ logWarn(
431
+ "plugin_user_prompt_contribution_budget_exceeded",
432
+ {},
433
+ {
434
+ "app.plugin.name": pluginName
435
+ },
436
+ "Plugin user prompt contributions exceeded budget"
437
+ );
438
+ continue;
439
+ }
440
+ totalChars += pluginContributionChars;
441
+ contributions.push(...acceptedContributions);
442
+ } catch (error) {
443
+ logWarn(
444
+ "plugin_user_prompt_hook_failed",
445
+ {},
446
+ {
447
+ "app.plugin.name": pluginName,
448
+ "exception.message": safeErrorMessage(error)
449
+ },
450
+ "Plugin user prompt hook failed"
451
+ );
452
+ }
453
+ }
454
+ return contributions;
455
+ }
456
+ function getPluginTools(context) {
457
+ const tools = {};
458
+ for (const plugin of getPlugins()) {
459
+ const pluginName = plugin.manifest.name;
460
+ const hook = plugin.hooks?.tools;
461
+ if (!hook) {
462
+ continue;
463
+ }
464
+ const slackToolContext = getSlackToolContext(context);
465
+ const credentialSubject = slackToolContext ? createSlackDirectCredentialSubject({
466
+ channelId: slackToolContext.sourceChannelId,
467
+ teamId: slackToolContext.teamId,
468
+ userId: slackToolContext.requester?.userId
469
+ }) : void 0;
470
+ const slackContext = slackToolContext ? {
471
+ channelCapabilities: resolveChannelCapabilities(
472
+ slackToolContext.sourceChannelId
473
+ ),
474
+ ...credentialSubject ? { credentialSubject } : {}
475
+ } : void 0;
476
+ let pluginContext;
477
+ if (context.source.platform === "slack") {
478
+ if (context.destination.platform !== "slack") {
479
+ throw new TypeError(
480
+ "Slack plugin tool context requires Slack destination"
481
+ );
482
+ }
483
+ pluginContext = {
484
+ ...basePluginContext(plugin),
485
+ requester: context.requester?.platform === "slack" ? context.requester : void 0,
486
+ conversationId: context.conversationId,
487
+ destination: context.destination,
488
+ slack: slackContext,
489
+ source: context.source,
490
+ userText: context.userText,
491
+ embedder: createPluginEmbedder(pluginName),
492
+ model: createPluginModel(pluginName, plugin.model),
493
+ state: createPluginState(pluginName)
494
+ };
495
+ } else {
496
+ if (context.destination.platform !== "local") {
497
+ throw new TypeError(
498
+ "Local plugin tool context requires local destination"
499
+ );
500
+ }
501
+ pluginContext = {
502
+ ...basePluginContext(plugin),
503
+ requester: context.requester?.platform === "local" ? context.requester : void 0,
504
+ conversationId: context.conversationId,
505
+ destination: context.destination,
506
+ source: context.source,
507
+ userText: context.userText,
508
+ embedder: createPluginEmbedder(pluginName),
509
+ model: createPluginModel(pluginName, plugin.model),
510
+ state: createPluginState(pluginName)
511
+ };
512
+ }
513
+ const pluginTools = hook(pluginContext);
514
+ for (const [name, tool] of Object.entries(pluginTools)) {
515
+ if (!PLUGIN_TOOL_NAME_RE.test(name)) {
516
+ throw new Error(
517
+ `Plugin tool "${name}" from plugin "${pluginName}" must be a camelCase identifier`
518
+ );
519
+ }
520
+ if (tools[name]) {
521
+ throw new Error(
522
+ `Duplicate plugin tool "${name}" from plugin "${pluginName}"`
523
+ );
524
+ }
525
+ tools[name] = tool;
526
+ }
527
+ }
528
+ return tools;
529
+ }
530
+ function routeMethods(route, pluginName) {
531
+ const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
532
+ if (methods.length === 0) {
533
+ throw new Error(
534
+ `Plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
535
+ );
536
+ }
537
+ for (const method of methods) {
538
+ if (!PLUGIN_ROUTE_METHODS.has(method)) {
539
+ throw new Error(
540
+ `Plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
541
+ );
542
+ }
543
+ }
544
+ if (methods.includes("ALL") && methods.length > 1) {
545
+ throw new Error(
546
+ `Plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
547
+ );
548
+ }
549
+ return methods;
550
+ }
551
+ function getPluginRoutes() {
552
+ const routes = [];
553
+ const seen = /* @__PURE__ */ new Set();
554
+ const methodsByPath = /* @__PURE__ */ new Map();
555
+ for (const plugin of getPlugins()) {
556
+ const pluginName = plugin.manifest.name;
557
+ const hook = plugin.hooks?.routes;
558
+ if (!hook) {
559
+ continue;
560
+ }
561
+ const pluginRoutes = hook({
562
+ ...basePluginContext(plugin)
563
+ });
564
+ if (!Array.isArray(pluginRoutes)) {
565
+ throw new Error(
566
+ `Plugin routes hook from plugin "${pluginName}" must return an array`
567
+ );
568
+ }
569
+ for (const route of pluginRoutes) {
570
+ if (!isRecord(route)) {
571
+ throw new Error(
572
+ `Plugin route from plugin "${pluginName}" must be an object`
573
+ );
574
+ }
575
+ if (typeof route.path !== "string" || !route.path.startsWith("/")) {
576
+ throw new Error(
577
+ `Plugin route "${route.path}" from plugin "${pluginName}" must start with /`
578
+ );
579
+ }
580
+ if (typeof route.handler !== "function") {
581
+ throw new Error(
582
+ `Plugin route "${route.path}" from plugin "${pluginName}" must provide a handler`
583
+ );
584
+ }
585
+ const methods = routeMethods(route, pluginName);
586
+ const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
587
+ if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
588
+ throw new Error(
589
+ `Plugin route "${route.path}" conflicts with an ALL route for the same path`
590
+ );
591
+ }
592
+ for (const method of methods) {
593
+ const key = `${method}:${route.path}`;
594
+ if (seen.has(key)) {
595
+ throw new Error(`Duplicate plugin route "${method} ${route.path}"`);
596
+ }
597
+ seen.add(key);
598
+ pathMethods.add(method);
599
+ }
600
+ methodsByPath.set(route.path, pathMethods);
601
+ routes.push({
602
+ ...route,
603
+ pluginName
604
+ });
605
+ }
606
+ }
607
+ return routes;
608
+ }
609
+ function trustedSlackConversationUrl(pluginName, link) {
610
+ const url = typeof link?.url === "string" ? link.url.trim() : "";
611
+ if (!url) {
612
+ return void 0;
613
+ }
614
+ let parsed;
615
+ try {
616
+ parsed = new URL(url);
617
+ } catch (error) {
618
+ throw new Error(
619
+ `Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
620
+ { cause: error }
621
+ );
622
+ }
623
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
624
+ throw new Error(
625
+ `Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
626
+ );
627
+ }
628
+ return parsed.toString();
629
+ }
630
+ function getPluginSlackConversationLink(conversationId) {
631
+ for (const plugin of getPlugins()) {
632
+ const pluginName = plugin.manifest.name;
633
+ const hook = plugin.hooks?.slackConversationLink;
634
+ if (!hook) {
635
+ continue;
636
+ }
637
+ const link = hook({
638
+ ...basePluginContext(plugin),
639
+ conversationId
640
+ });
641
+ const url = trustedSlackConversationUrl(pluginName, link);
642
+ if (url) {
643
+ return { url };
644
+ }
645
+ }
646
+ return void 0;
647
+ }
648
+ function pluginReadState(state) {
649
+ return {
650
+ get: state.get
651
+ };
652
+ }
653
+ function operationalReportText(value, maxLength) {
654
+ if (typeof value !== "string") {
655
+ return void 0;
656
+ }
657
+ const trimmed = value.trim();
658
+ if (!trimmed) {
659
+ return void 0;
660
+ }
661
+ return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
662
+ }
663
+ function operationalReportTone(tone) {
664
+ return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
665
+ }
666
+ function sanitizeOperationalReport(args) {
667
+ const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
668
+ const label = operationalReportText(
669
+ metric.label,
670
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
671
+ );
672
+ const value = operationalReportText(
673
+ metric.value,
674
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
675
+ );
676
+ if (!label || !value) {
677
+ return void 0;
678
+ }
679
+ const sanitizedMetric = { label, value };
680
+ const tone = operationalReportTone(metric.tone);
681
+ if (tone) {
682
+ sanitizedMetric.tone = tone;
683
+ }
684
+ return sanitizedMetric;
685
+ }).filter((metric) => Boolean(metric));
686
+ const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
687
+ const title2 = operationalReportText(
688
+ recordSet.title,
689
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
690
+ );
691
+ if (!title2) {
692
+ return void 0;
693
+ }
694
+ const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
695
+ const key = operationalReportText(
696
+ field.key,
697
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
698
+ );
699
+ const label = operationalReportText(
700
+ field.label,
701
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
702
+ );
703
+ return key && label ? { key, label } : void 0;
704
+ }).filter((field) => Boolean(field));
705
+ const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
706
+ const id = operationalReportText(
707
+ record.id,
708
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
709
+ ) ?? `${recordSetIndex}:${recordIndex}`;
710
+ const values = Object.fromEntries(
711
+ (fields ?? []).map((field) => [
712
+ field.key,
713
+ operationalReportText(
714
+ record.values[field.key],
715
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
716
+ ) ?? ""
717
+ ])
718
+ );
719
+ const sanitizedRecord = {
720
+ id,
721
+ values
722
+ };
723
+ const tone = operationalReportTone(record.tone);
724
+ if (tone) {
725
+ sanitizedRecord.tone = tone;
726
+ }
727
+ return sanitizedRecord;
728
+ });
729
+ const sanitizedRecordSet = { title: title2 };
730
+ if (fields?.length) {
731
+ sanitizedRecordSet.fields = fields;
732
+ }
733
+ const emptyText = operationalReportText(
734
+ recordSet.emptyText,
735
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
736
+ );
737
+ if (emptyText) {
738
+ sanitizedRecordSet.emptyText = emptyText;
739
+ }
740
+ if (records?.length) {
741
+ sanitizedRecordSet.records = records;
742
+ }
743
+ return sanitizedRecordSet;
744
+ }).filter(
745
+ (recordSet) => Boolean(recordSet)
746
+ );
747
+ const sanitized = {
748
+ pluginName: args.pluginName
749
+ };
750
+ const generatedAt = operationalReportText(
751
+ args.report.generatedAt,
752
+ OPERATIONAL_REPORT_MAX_VALUE_LENGTH
753
+ );
754
+ if (generatedAt) {
755
+ sanitized.generatedAt = generatedAt;
756
+ }
757
+ if (recordSets?.length) {
758
+ sanitized.recordSets = recordSets;
759
+ }
760
+ if (metrics?.length) {
761
+ sanitized.metrics = metrics;
762
+ }
763
+ const title = operationalReportText(
764
+ args.report.title,
765
+ OPERATIONAL_REPORT_MAX_LABEL_LENGTH
766
+ );
767
+ if (title) {
768
+ sanitized.title = title;
769
+ }
770
+ return sanitized;
771
+ }
772
+ function failedOperationalReport(args) {
773
+ return {
774
+ generatedAt: new Date(args.nowMs).toISOString(),
775
+ pluginName: args.pluginName,
776
+ metrics: [{ label: "report", tone: "danger", value: "failed" }],
777
+ title: args.pluginName,
778
+ recordSets: [
779
+ {
780
+ emptyText: "This plugin report failed to load.",
781
+ title: "Error"
782
+ }
783
+ ]
784
+ };
785
+ }
786
+ async function getPluginOperationalReports(nowMs, conversations) {
787
+ const reports = [];
788
+ for (const plugin of getPlugins()) {
789
+ const pluginName = plugin.manifest.name;
790
+ const hook = plugin.hooks?.operationalReport;
791
+ if (!hook) {
792
+ continue;
793
+ }
794
+ try {
795
+ const state = createPluginState(pluginName);
796
+ const report = await hook({
797
+ ...basePluginContext(plugin),
798
+ conversations,
799
+ nowMs,
800
+ state: pluginReadState(state)
801
+ });
802
+ if (!report) {
803
+ continue;
804
+ }
805
+ reports.push(
806
+ sanitizeOperationalReport({
807
+ pluginName,
808
+ report
809
+ })
810
+ );
811
+ } catch (error) {
812
+ const log = createPluginLogger(pluginName);
813
+ log.error("Plugin operational report failed", {
814
+ error: error instanceof Error ? error.message : String(error)
815
+ });
816
+ reports.push(failedOperationalReport({ nowMs, pluginName }));
817
+ }
818
+ }
819
+ return reports;
820
+ }
821
+ function normalizeEnv(value) {
822
+ if (!isRecord(value)) {
823
+ return {};
824
+ }
825
+ const env = {};
826
+ for (const [key, rawValue] of Object.entries(value)) {
827
+ if (typeof rawValue === "string") {
828
+ env[key] = rawValue;
829
+ }
830
+ }
831
+ return env;
832
+ }
833
+ function createSandboxCapability(sandbox) {
834
+ return {
835
+ root: SANDBOX_WORKSPACE_ROOT,
836
+ juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
837
+ async readFile(filePath) {
838
+ return await sandbox.readFileToBuffer({ path: filePath }) ?? null;
839
+ },
840
+ async run(input) {
841
+ const result = await sandbox.runCommand(input);
842
+ const [stdout, stderr] = await Promise.all([
843
+ result.stdout(),
844
+ result.stderr()
845
+ ]);
846
+ return {
847
+ exitCode: result.exitCode,
848
+ stdout,
849
+ stderr
850
+ };
851
+ },
852
+ async writeFile(input) {
853
+ await sandbox.writeFiles([
854
+ {
855
+ path: input.path,
856
+ content: input.content,
857
+ ...input.mode !== void 0 ? { mode: input.mode } : {}
858
+ }
859
+ ]);
860
+ }
861
+ };
862
+ }
863
+ function createPluginHookRunner(input = {}) {
864
+ const loaded = getPlugins();
865
+ return {
866
+ async prepareSandbox(sandbox) {
867
+ const sandboxCapability = createSandboxCapability(sandbox);
868
+ for (const plugin of loaded) {
869
+ const pluginName = plugin.manifest.name;
870
+ const hook = plugin.hooks?.sandboxPrepare;
871
+ if (!hook) {
872
+ continue;
873
+ }
874
+ logInfo(
875
+ "agent_plugin_hook_sandbox_prepare",
876
+ {},
877
+ { "app.plugin.name": pluginName },
878
+ "Running agent plugin sandbox prepare hook"
879
+ );
880
+ await hook({
881
+ ...basePluginContext(plugin),
882
+ requester: input.requester,
883
+ sandbox: sandboxCapability
884
+ });
885
+ }
886
+ },
887
+ async beforeToolExecute(tool) {
888
+ let nextInput = { ...tool.input };
889
+ const env = normalizeEnv(nextInput.env);
890
+ for (const plugin of loaded) {
891
+ const pluginName = plugin.manifest.name;
892
+ const hook = plugin.hooks?.beforeToolExecute;
893
+ if (!hook) {
894
+ continue;
895
+ }
896
+ let replacement;
897
+ let denied;
898
+ await hook({
899
+ ...basePluginContext(plugin),
900
+ requester: input.requester,
901
+ tool: {
902
+ name: tool.name,
903
+ input: nextInput
904
+ },
905
+ env: {
906
+ get(key) {
907
+ return env[key];
908
+ },
909
+ set(key, value) {
910
+ env[key] = value;
911
+ }
912
+ },
913
+ decision: {
914
+ deny(message) {
915
+ denied = message;
916
+ },
917
+ replaceInput(input2) {
918
+ replacement = input2;
919
+ }
920
+ }
921
+ });
922
+ if (denied) {
923
+ throw new PluginHookDeniedError(denied);
924
+ }
925
+ if (replacement !== void 0) {
926
+ if (!isRecord(replacement)) {
927
+ throw new Error(
928
+ `Plugin "${pluginName}" replaced tool input with a non-object value`
929
+ );
930
+ }
931
+ nextInput = { ...replacement };
932
+ Object.assign(env, normalizeEnv(nextInput.env));
933
+ }
934
+ }
935
+ return {
936
+ input: {
937
+ ...nextInput,
938
+ ...Object.keys(env).length > 0 ? { env } : {}
939
+ },
940
+ env
941
+ };
942
+ }
943
+ };
944
+ }
945
+
946
+ export {
947
+ createPluginModel,
948
+ createPluginEmbedder,
949
+ getSlackToolContext,
950
+ bindSlackDirectCredentialSubject,
951
+ verifySlackDirectCredentialSubject,
952
+ resolveChannelCapabilities,
953
+ PluginHookDeniedError,
954
+ validatePlugins,
955
+ setPlugins,
956
+ getPlugins,
957
+ getPluginSystemPromptContributions,
958
+ getPluginUserPromptContributions,
959
+ getPluginTools,
960
+ getPluginRoutes,
961
+ getPluginSlackConversationLink,
962
+ getPluginOperationalReports,
963
+ createPluginHookRunner
964
+ };