@sentry/junior 0.202.0 → 0.204.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 (174) hide show
  1. package/dist/agent-CMyTD4DD.js +17509 -0
  2. package/dist/agent-hooks-Bbcp9e33.js +4040 -0
  3. package/dist/agent-runner-DNb1A49M.js +678 -0
  4. package/dist/api/schema.d.ts +2728 -2639
  5. package/dist/api/schema.js +4 -169
  6. package/dist/api.d.ts +23 -17
  7. package/dist/api.js +3881 -5326
  8. package/dist/app-B1TOqBI8.d.ts +246 -0
  9. package/dist/app.d.ts +4 -520
  10. package/dist/app.js +9522 -11875
  11. package/dist/auth-pause-state-DosEalIG.js +168 -0
  12. package/dist/catalog-runtime-BReos-D4.js +7 -0
  13. package/dist/checkpoint-CNrPyRJN.js +2450 -0
  14. package/dist/cli/chat.d.ts +20 -0
  15. package/dist/cli/chat.js +229 -336
  16. package/dist/cli/check.d.ts +8 -0
  17. package/dist/cli/check.js +513 -806
  18. package/dist/cli/env.d.ts +13 -0
  19. package/dist/cli/env.js +63 -12
  20. package/dist/cli/init.d.ts +3 -0
  21. package/dist/cli/init.js +90 -159
  22. package/dist/cli/main.d.ts +6 -0
  23. package/dist/cli/main.js +43 -64
  24. package/dist/cli/plugins.d.ts +13 -0
  25. package/dist/cli/plugins.js +148 -227
  26. package/dist/cli/run.d.ts +16 -0
  27. package/dist/cli/run.js +52 -8
  28. package/dist/cli/snapshot-create.d.ts +4 -0
  29. package/dist/cli/snapshot-create.js +71 -125
  30. package/dist/cli/upgrade.d.ts +15 -0
  31. package/dist/cli/upgrade.js +232 -335
  32. package/dist/config-nGCW1OTG.js +941 -0
  33. package/dist/context-B45_WqVX.js +298 -0
  34. package/dist/conversation-privacy-B4voFRSm.js +182 -0
  35. package/dist/conversation-work-C6y3T5Hx.js +51 -0
  36. package/dist/credential-sync-kXr1hvO1.js +2660 -0
  37. package/dist/db-BeyXKJOI.d.ts +5 -0
  38. package/dist/destinations-BeBiJeEM.js +29 -0
  39. package/dist/execution-stats-DWfSihcH.js +373 -0
  40. package/dist/executor-DcMSlI5c.js +721 -0
  41. package/dist/experimental-CIXf7Uye.js +46 -0
  42. package/dist/history-ClE4toxe.d.ts +32 -0
  43. package/dist/history-CqTuSAdI.js +441 -0
  44. package/dist/instrumentation.d.ts +3 -3
  45. package/dist/instrumentation.js +130 -176
  46. package/dist/job-queue-Drn_LXlM.js +63 -0
  47. package/dist/logging-BzAk1S_s.js +1210 -0
  48. package/dist/model-profile-CZmVoGdX.js +49 -0
  49. package/dist/nitro.d.ts +31 -36
  50. package/dist/nitro.js +275 -456
  51. package/dist/oauth-callback-server-JiftokvA.js +130 -0
  52. package/dist/oauth-relay-CdCnu80W.js +62 -0
  53. package/dist/package-discovery-7a9YqLhE.js +455 -0
  54. package/dist/pending-auth-CWmSdZVg.js +185 -0
  55. package/dist/plugin-module-BKGOXca2.js +81 -0
  56. package/dist/plugins-BI9A1vKA.d.ts +95 -0
  57. package/dist/plugins-egKWjgue.js +99 -0
  58. package/dist/private-trace-result-BLvBjy_e.js +16 -0
  59. package/dist/profile-CrT5nXbw.js +861 -0
  60. package/dist/registry-DH1xUo2N.js +1419 -0
  61. package/dist/reporting-schema-DosTN__Q.js +105 -0
  62. package/dist/resolve-CZHnfxaS.js +546 -0
  63. package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
  64. package/dist/runner-CZV2sC46.js +392 -0
  65. package/dist/sandbox-egress-signals-bHZfmhbd.js +46 -0
  66. package/dist/schema-Rn8-XEaN.js +1140 -0
  67. package/dist/sentry-CD3SdJl-.js +26 -0
  68. package/dist/skills-CIcDQSgM.js +316 -0
  69. package/dist/spawn-DdkUFRmq.js +29 -0
  70. package/dist/stats-DTUbAb88.js +2634 -0
  71. package/dist/store-C5qxF2ac.d.ts +208 -0
  72. package/dist/task-queue-CJIR4RF7.js +136 -0
  73. package/dist/task-runner-D-O3A-Cp.js +639 -0
  74. package/dist/thread-state-CRc9jTJS.js +431 -0
  75. package/dist/turn-execution-BkinIdJF.js +47 -0
  76. package/dist/turn-lifecycle-DQV19VTW.js +54 -0
  77. package/dist/usage-D3EbSO7w.js +296 -0
  78. package/dist/usage-schema-CKaDkH_j.js +20 -0
  79. package/dist/validation-C2CR-OX3.js +40 -0
  80. package/dist/vercel-queue-DIkVcfK4.js +167 -0
  81. package/dist/vercel.d.ts +6 -6
  82. package/dist/vercel.js +14 -6
  83. package/dist/version-CnXZ614E.d.ts +5 -0
  84. package/dist/version-DxcWdoKa.js +8 -0
  85. package/dist/version.d.ts +2 -4
  86. package/dist/version.js +2 -6
  87. package/dist/web-authorization-RMBxAR_t.js +76 -0
  88. package/dist/web-input-3EPfpf55.js +494 -0
  89. package/dist/work-fGofI7Op.js +509 -0
  90. package/dist/worker-CsOLq-mR.js +625 -0
  91. package/package.json +9 -9
  92. package/dist/agent-hooks-UXITLYOK.js +0 -51
  93. package/dist/catalog-runtime-BG3JLLK2.js +0 -14
  94. package/dist/chunk-2LYHWMR4.js +0 -46
  95. package/dist/chunk-2MK3QBGA.js +0 -321
  96. package/dist/chunk-3PGQSKJV.js +0 -692
  97. package/dist/chunk-3UI3WFTX.js +0 -519
  98. package/dist/chunk-3Y47VMHV.js +0 -6
  99. package/dist/chunk-4UO6FK4G.js +0 -64
  100. package/dist/chunk-5NOPO64K.js +0 -198
  101. package/dist/chunk-5PKWYPFT.js +0 -2943
  102. package/dist/chunk-63NKKFJL.js +0 -1228
  103. package/dist/chunk-6AYH6252.js +0 -787
  104. package/dist/chunk-6Y5B5PEX.js +0 -80
  105. package/dist/chunk-7DHZP5KJ.js +0 -333
  106. package/dist/chunk-7H2VZTWN.js +0 -103
  107. package/dist/chunk-7NL7T6VU.js +0 -406
  108. package/dist/chunk-A76OIMYN.js +0 -102
  109. package/dist/chunk-AWMS3SPP.js +0 -58
  110. package/dist/chunk-B3LY5RCL.js +0 -229
  111. package/dist/chunk-BD2KT3OB.js +0 -36
  112. package/dist/chunk-BXCX5QKA.js +0 -2086
  113. package/dist/chunk-C6VVF43U.js +0 -81
  114. package/dist/chunk-CEMMLV5L.js +0 -98
  115. package/dist/chunk-CM5HNV2W.js +0 -131
  116. package/dist/chunk-DA2KGZZI.js +0 -13
  117. package/dist/chunk-DBVY6HLX.js +0 -64
  118. package/dist/chunk-DIB4GX3G.js +0 -8
  119. package/dist/chunk-DKIJ2RP5.js +0 -68
  120. package/dist/chunk-DL2KLKVH.js +0 -931
  121. package/dist/chunk-DWPN6SCH.js +0 -172
  122. package/dist/chunk-ELWOYVOY.js +0 -206
  123. package/dist/chunk-EWT7H65X.js +0 -506
  124. package/dist/chunk-FA6POGLH.js +0 -70
  125. package/dist/chunk-HMREVIO2.js +0 -135
  126. package/dist/chunk-IIWUT4FO.js +0 -820
  127. package/dist/chunk-JAL3CH4G.js +0 -27
  128. package/dist/chunk-JSJQEWBJ.js +0 -3295
  129. package/dist/chunk-KHKKHDLD.js +0 -387
  130. package/dist/chunk-LA6R4A7Q.js +0 -706
  131. package/dist/chunk-LOOU2DUR.js +0 -20428
  132. package/dist/chunk-M6FHWXE3.js +0 -700
  133. package/dist/chunk-M76HMB7M.js +0 -1856
  134. package/dist/chunk-MAGNK5J4.js +0 -1520
  135. package/dist/chunk-MTM5IFCY.js +0 -1190
  136. package/dist/chunk-N7TMSOZH.js +0 -235
  137. package/dist/chunk-NHJU2ZAL.js +0 -56
  138. package/dist/chunk-NSXKRGRT.js +0 -717
  139. package/dist/chunk-OWMPFXJA.js +0 -1764
  140. package/dist/chunk-PDO5BLNM.js +0 -22
  141. package/dist/chunk-PEQXYSSE.js +0 -45
  142. package/dist/chunk-PW4P4JYF.js +0 -1135
  143. package/dist/chunk-QB54UQUS.js +0 -320
  144. package/dist/chunk-QP3OGJXF.js +0 -105
  145. package/dist/chunk-RC745GGZ.js +0 -122
  146. package/dist/chunk-REOIRYPR.js +0 -37
  147. package/dist/chunk-SCOZRLZ4.js +0 -164
  148. package/dist/chunk-TXJSXBV5.js +0 -1433
  149. package/dist/chunk-TXKUKMVO.js +0 -417
  150. package/dist/chunk-U2GPUYAL.js +0 -3656
  151. package/dist/chunk-UIE3R5XU.js +0 -17
  152. package/dist/chunk-UXPG6ZIN.js +0 -84
  153. package/dist/chunk-VFUK3X5B.js +0 -50
  154. package/dist/chunk-WQSHDTTI.js +0 -1163
  155. package/dist/chunk-X6GDDWQH.js +0 -60
  156. package/dist/chunk-Y2CM7HXH.js +0 -111
  157. package/dist/chunk-Y7X25LFY.js +0 -15
  158. package/dist/chunk-YDUR3ZYW.js +0 -10
  159. package/dist/conversation-work-VIOQ5ZF3.js +0 -54
  160. package/dist/db-3L2MCVRI.js +0 -26
  161. package/dist/discovery-DWVDM7UK.js +0 -44
  162. package/dist/experimental-P5FZM4CU.js +0 -12
  163. package/dist/oauth-callback-server-YVV6C6T2.js +0 -243
  164. package/dist/oauth-relay-5XPVRPYB.js +0 -8
  165. package/dist/plugins-YTYJMMJF.js +0 -14
  166. package/dist/runner-LQ7RETDP.js +0 -522
  167. package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
  168. package/dist/sentry-MB3NL3IR.js +0 -42
  169. package/dist/skills-XRWYEDCD.js +0 -27
  170. package/dist/spawn-RC6SJUAI.js +0 -42
  171. package/dist/task-runner-7DY7ARYH.js +0 -36
  172. package/dist/validation-7MNQGCJA.js +0 -17
  173. package/dist/work-COALHUCG.js +0 -51
  174. package/dist/worker-6MURZIPD.js +0 -24
@@ -1,3656 +0,0 @@
1
- import {
2
- JUNIOR_THREAD_STATE_TTL_MS
3
- } from "./chunk-3Y47VMHV.js";
4
- import {
5
- getConversationEventStore,
6
- getDb,
7
- getSqlExecutor
8
- } from "./chunk-M76HMB7M.js";
9
- import {
10
- juniorCodeChanges,
11
- juniorCodeRepositories,
12
- juniorConversationAnnotations,
13
- juniorConversationEvents,
14
- juniorIdentities,
15
- juniorSnapshots,
16
- juniorUsers,
17
- juniorWorkspaceRepos,
18
- juniorWorkspaces
19
- } from "./chunk-PW4P4JYF.js";
20
- import {
21
- SANDBOX_WORKSPACE_ROOT,
22
- getVercelSandboxCredentials,
23
- hash,
24
- isSandboxMissingError,
25
- runNonInteractiveCommand,
26
- sleep
27
- } from "./chunk-NSXKRGRT.js";
28
- import {
29
- getStateAdapter
30
- } from "./chunk-2MK3QBGA.js";
31
- import {
32
- botConfig,
33
- completeObject,
34
- embedTexts,
35
- getSlackBotToken
36
- } from "./chunk-63NKKFJL.js";
37
- import {
38
- defaultModelId
39
- } from "./chunk-NHJU2ZAL.js";
40
- import {
41
- isActorUserId,
42
- parseActorUserId,
43
- parseSlackChannelReferenceId,
44
- parseSlackMessageTs,
45
- parseSlackTeamId
46
- } from "./chunk-N7TMSOZH.js";
47
- import {
48
- logException,
49
- logInfo,
50
- logWarn,
51
- normalizeIdentityEmail,
52
- setSpanAttributes,
53
- setSpanStatus,
54
- withSpan
55
- } from "./chunk-MAGNK5J4.js";
56
-
57
- // src/chat/plugins/agent-hooks.ts
58
- import {
59
- missingToolAnnotationKeys,
60
- normalizeResourceEventIdentifier,
61
- pluginResourceEventsSchema,
62
- promptContextSchema,
63
- promptMessageSchema,
64
- resourceEventInputSchema
65
- } from "@sentry/junior-plugin-api";
66
-
67
- // src/chat/plugins/annotations.ts
68
- import { and, desc, eq, inArray } from "drizzle-orm";
69
- import {
70
- conversationAnnotationInputSchema
71
- } from "@sentry/junior-plugin-api";
72
- function annotationFromRow(row) {
73
- const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
74
- if (!parsed.success) return void 0;
75
- return {
76
- ...parsed.data,
77
- plugin: row.plugin,
78
- createdAt: row.createdAt.toISOString(),
79
- updatedAt: row.updatedAt.toISOString()
80
- };
81
- }
82
- function createPluginAnnotations(args) {
83
- return {
84
- async upsert(input) {
85
- const annotation = conversationAnnotationInputSchema.parse(input);
86
- const now = /* @__PURE__ */ new Date();
87
- await args.db.insert(juniorConversationAnnotations).values({
88
- annotation,
89
- conversationId: args.conversationId,
90
- createdAt: now,
91
- key: annotation.key,
92
- kind: annotation.kind,
93
- plugin: args.plugin,
94
- updatedAt: now
95
- }).onConflictDoUpdate({
96
- target: [
97
- juniorConversationAnnotations.conversationId,
98
- juniorConversationAnnotations.plugin,
99
- juniorConversationAnnotations.kind,
100
- juniorConversationAnnotations.key
101
- ],
102
- set: { annotation, updatedAt: now }
103
- });
104
- },
105
- async remove(kind, key) {
106
- await args.db.delete(juniorConversationAnnotations).where(
107
- and(
108
- eq(
109
- juniorConversationAnnotations.conversationId,
110
- args.conversationId
111
- ),
112
- eq(juniorConversationAnnotations.plugin, args.plugin),
113
- eq(juniorConversationAnnotations.kind, kind),
114
- eq(juniorConversationAnnotations.key, key)
115
- )
116
- );
117
- },
118
- async list() {
119
- return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
120
- }
121
- };
122
- }
123
- async function listConversationAnnotations(db, conversationId) {
124
- const byConversation = await listConversationAnnotationsById(db, [
125
- conversationId
126
- ]);
127
- return byConversation.get(conversationId) ?? [];
128
- }
129
- async function listConversationAnnotationsById(db, conversationIds) {
130
- const byConversation = /* @__PURE__ */ new Map();
131
- if (conversationIds.length === 0) return byConversation;
132
- const rows = await db.select().from(juniorConversationAnnotations).where(
133
- inArray(juniorConversationAnnotations.conversationId, [
134
- ...conversationIds
135
- ])
136
- ).orderBy(desc(juniorConversationAnnotations.updatedAt));
137
- for (const row of rows) {
138
- const annotation = annotationFromRow(row);
139
- if (!annotation) continue;
140
- const current = byConversation.get(row.conversationId);
141
- if (current) {
142
- current.push(annotation);
143
- } else {
144
- byConversation.set(row.conversationId, [annotation]);
145
- }
146
- }
147
- return byConversation;
148
- }
149
-
150
- // src/chat/plugins/conversation-events.ts
151
- function registeredDefinition(plugin, value) {
152
- return plugin.conversationEvents?.find(
153
- (definition) => definition === value.definition
154
- );
155
- }
156
- function eventIdentityVersion(plugin, definition) {
157
- let version = definition.version;
158
- for (const candidate of plugin.conversationEvents ?? []) {
159
- if (candidate.eventName === definition.eventName) {
160
- version = Math.min(version, candidate.version);
161
- }
162
- }
163
- return version;
164
- }
165
- function createPluginConversationEvents(args) {
166
- return {
167
- async emit(value) {
168
- const definition = registeredDefinition(args.plugin, value);
169
- if (!definition) {
170
- throw new Error(
171
- `Plugin "${args.plugin.manifest.name}" cannot emit an unregistered conversation event.`
172
- );
173
- }
174
- const content = definition.parse(value.data);
175
- const identityVersion = eventIdentityVersion(args.plugin, definition);
176
- await getConversationEventStore().append(
177
- args.conversationId,
178
- [
179
- {
180
- createdAtMs: Date.now(),
181
- idempotencyKey: `plugin:${args.plugin.manifest.name}:operation:${args.operationId}:event:${definition.eventName}@${identityVersion}`,
182
- data: {
183
- type: "structured_event",
184
- namespace: args.plugin.manifest.name,
185
- name: definition.eventName,
186
- version: definition.version,
187
- turnId: args.turnId,
188
- content
189
- }
190
- }
191
- ],
192
- { activity: "preserve" }
193
- );
194
- }
195
- };
196
- }
197
- function renderPluginConversationEvent(args) {
198
- const plugin = getPlugins().find(
199
- (candidate) => candidate.manifest.name === args.namespace
200
- );
201
- const definition = plugin?.conversationEvents?.find(
202
- (candidate) => candidate.eventName === args.name && candidate.version === args.version
203
- );
204
- if (!definition) return void 0;
205
- return definition.renderEvent(definition.parse(args.content));
206
- }
207
-
208
- // src/chat/plugins/conversation-event-stats.ts
209
- import { and as and2, eq as eq2, gte, lt, sql } from "drizzle-orm";
210
- import { z } from "zod";
211
- var DAY_MS = 24 * 60 * 60 * 1e3;
212
- var costRowSchema = z.object({
213
- costUsd: z.number().finite().nonnegative(),
214
- date: z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2})?$/),
215
- events: z.number().int().nonnegative()
216
- }).strict();
217
- function startOfUtcDay(value) {
218
- const date = new Date(value);
219
- date.setUTCHours(0, 0, 0, 0);
220
- return date;
221
- }
222
- function registeredEventName(plugin, eventName) {
223
- if (!plugin.conversationEvents?.some(
224
- (definition) => definition.eventName === eventName
225
- )) {
226
- throw new TypeError(
227
- `Plugin "${plugin.manifest.name}" did not register event "${eventName}"`
228
- );
229
- }
230
- return eventName;
231
- }
232
- function createPluginConversationEventStats(plugin, now = Date.now) {
233
- return {
234
- async costsByHour(input) {
235
- const eventName = registeredEventName(plugin, input.eventName);
236
- const hourCount = input.hours ?? 24;
237
- const end = new Date(now());
238
- end.setUTCMinutes(0, 0, 0);
239
- const start = new Date(end.getTime() - (hourCount - 1) * 60 * 60 * 1e3);
240
- const endExclusive = new Date(end.getTime() + 60 * 60 * 1e3);
241
- const date = sql`TO_CHAR(
242
- ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
243
- 'YYYY-MM-DD"T"HH24'
244
- )`;
245
- const cost = sql`CASE
246
- WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
247
- THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
248
- ELSE 0
249
- END`;
250
- const rows = await getDb().select({
251
- costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
252
- date,
253
- events: sql`COUNT(*)::integer`.mapWith(Number)
254
- }).from(juniorConversationEvents).where(
255
- and2(
256
- eq2(juniorConversationEvents.type, "structured_event"),
257
- sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`,
258
- sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`,
259
- gte(juniorConversationEvents.createdAt, start),
260
- lt(juniorConversationEvents.createdAt, endExclusive)
261
- )
262
- ).groupBy(date);
263
- const byHour = new Map(
264
- z.array(costRowSchema).parse(rows).map((row) => [row.date, row])
265
- );
266
- return Array.from({ length: hourCount }, (_, index) => {
267
- const hour = new Date(start.getTime() + index * 60 * 60 * 1e3);
268
- const key = hour.toISOString().slice(0, 13);
269
- return byHour.get(key) ?? {
270
- costUsd: 0,
271
- date: key,
272
- events: 0
273
- };
274
- });
275
- },
276
- async costsByDay(input) {
277
- const eventName = registeredEventName(plugin, input.eventName);
278
- const end = startOfUtcDay(now());
279
- const start = new Date(end.getTime() - (input.days - 1) * DAY_MS);
280
- const endExclusive = new Date(end.getTime() + DAY_MS);
281
- const date = sql`TO_CHAR(
282
- ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
283
- 'YYYY-MM-DD'
284
- )`;
285
- const cost = sql`CASE
286
- WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
287
- THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
288
- ELSE 0
289
- END`;
290
- const rows = await getDb().select({
291
- costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
292
- date,
293
- events: sql`COUNT(*)::integer`.mapWith(Number)
294
- }).from(juniorConversationEvents).where(
295
- and2(
296
- eq2(juniorConversationEvents.type, "structured_event"),
297
- sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`,
298
- sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`,
299
- gte(juniorConversationEvents.createdAt, start),
300
- lt(juniorConversationEvents.createdAt, endExclusive)
301
- )
302
- ).groupBy(date);
303
- const byDate = new Map(
304
- z.array(costRowSchema).parse(rows).map((row) => [row.date, row])
305
- );
306
- return Array.from({ length: input.days }, (_, index) => {
307
- const day = new Date(start.getTime() + index * DAY_MS);
308
- const date2 = day.toISOString().slice(0, 10);
309
- return byDate.get(date2) ?? {
310
- costUsd: 0,
311
- date: date2,
312
- events: 0
313
- };
314
- });
315
- }
316
- };
317
- }
318
-
319
- // src/chat/plugins/logging.ts
320
- function createPluginLogger(plugin) {
321
- return {
322
- info(message, metadata) {
323
- logInfo("plugin.log.info", {
324
- "app.log.message": message,
325
- "app.plugin.name": plugin,
326
- ...metadata
327
- });
328
- },
329
- warn(message, metadata) {
330
- logWarn("plugin.log.warn", {
331
- "app.log.message": message,
332
- "app.plugin.name": plugin,
333
- ...metadata
334
- });
335
- },
336
- error(message, metadata) {
337
- logException(new Error(message), "plugin.log.error", {
338
- "app.plugin.name": plugin,
339
- ...metadata
340
- });
341
- }
342
- };
343
- }
344
-
345
- // src/chat/plugins/model.ts
346
- function createPluginModel(pluginName, options = {}, runtime = {}) {
347
- return {
348
- async completeObject(input) {
349
- const modelId = options.structuredModelId ?? (options.structuredModel === "default" ? defaultModelId(botConfig) : botConfig.fastModelId);
350
- const result = await completeObject({
351
- modelId,
352
- schema: input.schema,
353
- prompt: input.prompt,
354
- ...input.system !== void 0 ? { system: input.system } : void 0,
355
- ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : void 0,
356
- signal: runtime.signal,
357
- promptName: `${pluginName}.structured_completion`,
358
- metadata: {
359
- pluginName,
360
- pluginModelRole: "structured"
361
- }
362
- });
363
- return {
364
- object: result.object,
365
- ...result.costUsd !== void 0 ? { costUsd: result.costUsd } : void 0
366
- };
367
- }
368
- };
369
- }
370
- function createPluginEmbedder(pluginName, runtime = {}) {
371
- return {
372
- async embedTexts(input) {
373
- return await embedTexts({
374
- modelId: botConfig.embeddingModelId,
375
- texts: input.texts,
376
- signal: runtime.signal,
377
- metadata: {
378
- pluginName,
379
- pluginModelRole: "embedding"
380
- }
381
- });
382
- }
383
- };
384
- }
385
-
386
- // src/chat/plugins/state.ts
387
- import { createHash } from "crypto";
388
- var MAX_PLUGIN_STATE_KEY_LENGTH = 512;
389
- function hashKeyPart(value) {
390
- return createHash("sha256").update(value).digest("hex").slice(0, 32);
391
- }
392
- function pluginStateKey(plugin, key) {
393
- const pluginPrefix = `junior:${plugin}`;
394
- if (key === pluginPrefix || key.startsWith(`${pluginPrefix}:`)) {
395
- return key;
396
- }
397
- return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key)}`;
398
- }
399
- function validatePluginStateKey(key) {
400
- if (!key.trim()) {
401
- throw new Error("Plugin state key is required");
402
- }
403
- if (key.length > MAX_PLUGIN_STATE_KEY_LENGTH) {
404
- throw new Error("Plugin state key exceeds the maximum length");
405
- }
406
- }
407
- function createPluginState(plugin, adapter) {
408
- const getAdapter = () => adapter ?? getStateAdapter();
409
- return {
410
- async delete(key) {
411
- validatePluginStateKey(key);
412
- const state = getAdapter();
413
- await state.connect();
414
- await state.delete(pluginStateKey(plugin, key));
415
- },
416
- async get(key) {
417
- validatePluginStateKey(key);
418
- const state = getAdapter();
419
- await state.connect();
420
- const value = await state.get(pluginStateKey(plugin, key));
421
- return value ?? void 0;
422
- },
423
- async set(key, value, ttlMs) {
424
- validatePluginStateKey(key);
425
- const state = getAdapter();
426
- await state.connect();
427
- await state.set(pluginStateKey(plugin, key), value, ttlMs);
428
- },
429
- async setIfNotExists(key, value, ttlMs) {
430
- validatePluginStateKey(key);
431
- const state = getAdapter();
432
- await state.connect();
433
- return await state.setIfNotExists(
434
- pluginStateKey(plugin, key),
435
- value,
436
- ttlMs
437
- );
438
- },
439
- async withLock(key, ttlMs, callback) {
440
- validatePluginStateKey(key);
441
- const state = getAdapter();
442
- await state.connect();
443
- const lockKey = pluginStateKey(plugin, key);
444
- const lock = await state.acquireLock(lockKey, ttlMs);
445
- if (!lock) {
446
- throw new Error(`Could not acquire plugin state lock for ${key}`);
447
- }
448
- try {
449
- return await callback();
450
- } finally {
451
- await state.releaseLock(lock);
452
- }
453
- }
454
- };
455
- }
456
-
457
- // src/chat/slack/dashboard-link.ts
458
- var dashboardConversationLinkOptions;
459
- function withHttps(host) {
460
- return /^https?:\/\//.test(host) ? host : `https://${host}`;
461
- }
462
- function stripTrailingSlashes(value) {
463
- let end = value.length;
464
- while (end > 1 && value.charCodeAt(end - 1) === 47) {
465
- end -= 1;
466
- }
467
- return end === value.length ? value : value.slice(0, end);
468
- }
469
- function normalizeDashboardPath(path, fallback) {
470
- const value = path?.trim() || fallback;
471
- const withSlash = value.startsWith("/") ? value : `/${value}`;
472
- return stripTrailingSlashes(withSlash);
473
- }
474
- function resolveDashboardBaseURL(config) {
475
- const explicit = config.baseURL ?? process.env.JUNIOR_BASE_URL;
476
- if (explicit?.trim()) {
477
- return stripTrailingSlashes(withHttps(explicit.trim()));
478
- }
479
- const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
480
- if (vercelProd) {
481
- return stripTrailingSlashes(withHttps(vercelProd));
482
- }
483
- const vercelUrl = process.env.VERCEL_URL?.trim();
484
- if (vercelUrl) {
485
- return stripTrailingSlashes(withHttps(vercelUrl));
486
- }
487
- return "http://localhost:3000";
488
- }
489
- function setDashboardConversationLinkOptions(options) {
490
- const previous = dashboardConversationLinkOptions;
491
- dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
492
- return previous;
493
- }
494
- function resolveDashboardPath(segmentPath) {
495
- if (!dashboardConversationLinkOptions) {
496
- return void 0;
497
- }
498
- const baseURL = resolveDashboardBaseURL(dashboardConversationLinkOptions);
499
- const basePath = normalizeDashboardPath(
500
- dashboardConversationLinkOptions.basePath,
501
- "/"
502
- );
503
- const path = basePath === "/" ? segmentPath : `${basePath}${segmentPath}`;
504
- return `${baseURL}${path}`;
505
- }
506
- function getDashboardConversationLink(conversationId) {
507
- return resolveDashboardPath(
508
- `/conversations/${encodeURIComponent(conversationId)}`
509
- );
510
- }
511
- function getDashboardTaskLink(taskId) {
512
- return resolveDashboardPath(`/tasks/${encodeURIComponent(taskId)}`);
513
- }
514
-
515
- // src/chat/resource-events/store.ts
516
- import { createHash as createHash2 } from "crypto";
517
- import {
518
- resourceEventMatchSchema,
519
- resourceEventMatches,
520
- resourceEventTypeSchema,
521
- stableResourceEventMatchKey
522
- } from "@sentry/junior-plugin-api";
523
- import { z as z2 } from "zod";
524
- var RESOURCE_EVENT_PREFIX = "junior:resource_event_subscription:v5";
525
- var INDEX_LOCK_TTL_MS = 1e4;
526
- var SUBSCRIPTION_LOCK_TTL_MS = 1e4;
527
- var SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
528
- var SUBSCRIPTION_LOCK_RETRY_MS = 25;
529
- var SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
530
- var subscriptionStatusSchema = z2.enum(["active", "cancelled", "completed"]);
531
- var subscriptionIdIndexSchema = z2.array(z2.string().min(1));
532
- var subscriptionSchema = z2.object({
533
- conversationId: z2.string().min(1),
534
- createdAtMs: z2.number().finite(),
535
- events: z2.array(resourceEventTypeSchema).min(1),
536
- expiresAtMs: z2.number().finite(),
537
- id: z2.string().min(1),
538
- intent: z2.string().min(1),
539
- label: z2.string().min(1),
540
- match: resourceEventMatchSchema.optional(),
541
- namespace: z2.string().min(1),
542
- identifier: z2.string().min(1),
543
- resourceType: z2.string().min(1),
544
- status: subscriptionStatusSchema,
545
- updatedAtMs: z2.number().finite()
546
- }).strict();
547
- function digest(value) {
548
- return createHash2("sha256").update(value).digest("hex").slice(0, 32);
549
- }
550
- function subscriptionKey(id) {
551
- return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
552
- }
553
- function subscriptionLockKey(id) {
554
- return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
555
- }
556
- function resourceIndexKey(namespace, identifier) {
557
- return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${namespace}\0${identifier}`)}`;
558
- }
559
- function conversationIndexKey(conversationId) {
560
- return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
561
- }
562
- function indexLockKey(key) {
563
- return `${key}:lock`;
564
- }
565
- async function sleep2(ms) {
566
- await new Promise((resolve) => {
567
- const timer = setTimeout(resolve, ms);
568
- timer.unref?.();
569
- });
570
- }
571
- async function acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = Date.now() + SUBSCRIPTION_LOCK_WAIT_MS) {
572
- while (true) {
573
- const lock = await state.acquireLock(
574
- subscriptionLockKey(subscriptionId),
575
- SUBSCRIPTION_LOCK_TTL_MS
576
- );
577
- if (lock) {
578
- return lock;
579
- }
580
- if (Date.now() >= waitDeadlineMs) {
581
- throw new Error(
582
- `Could not acquire resource event subscription lock for ${subscriptionId}`
583
- );
584
- }
585
- await sleep2(SUBSCRIPTION_LOCK_RETRY_MS);
586
- }
587
- }
588
- async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
589
- const lock = await acquireSubscriptionLock(
590
- state,
591
- subscriptionId,
592
- waitDeadlineMs
593
- );
594
- let lockLostError;
595
- let heartbeatError;
596
- let heartbeat;
597
- const timer = setInterval(() => {
598
- if (heartbeat) {
599
- return;
600
- }
601
- heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
602
- if (extended) {
603
- heartbeatError = void 0;
604
- } else {
605
- lockLostError = new Error(
606
- `Resource event subscription lock was lost for ${subscriptionId}`
607
- );
608
- }
609
- }).catch((error) => {
610
- heartbeatError = error instanceof Error ? error : new Error(`Failed to extend subscription lock: ${String(error)}`);
611
- }).finally(() => {
612
- heartbeat = void 0;
613
- });
614
- }, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
615
- timer.unref?.();
616
- let result;
617
- try {
618
- result = await callback();
619
- } finally {
620
- clearInterval(timer);
621
- await heartbeat;
622
- await state.releaseLock(lock);
623
- }
624
- const lockError = lockLostError ?? heartbeatError;
625
- if (lockError) {
626
- throw lockError;
627
- }
628
- return result;
629
- }
630
- function ttlUntil(expiresAtMs, nowMs) {
631
- return Math.max(1, expiresAtMs - nowMs);
632
- }
633
- async function readSubscriptionIdIndex(state, key) {
634
- const value = await state.get(key);
635
- if (value === void 0 || value === null) {
636
- return [];
637
- }
638
- return subscriptionIdIndexSchema.parse(value);
639
- }
640
- function buildSubscriptionId(input) {
641
- const eventKey = [...new Set(input.events)].sort().join("\0");
642
- const matchKey = stableResourceEventMatchKey(input.match);
643
- const material = matchKey ? `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`;
644
- return `resub_${digest(material)}`;
645
- }
646
- async function withIndexLock(state, key, callback) {
647
- const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
648
- if (!lock) {
649
- throw new Error(`Could not acquire resource event index lock for ${key}`);
650
- }
651
- try {
652
- return await callback();
653
- } finally {
654
- await state.releaseLock(lock);
655
- }
656
- }
657
- async function addToIndex(state, key, subscriptionId, nowMs) {
658
- await withIndexLock(state, key, async () => {
659
- const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
660
- const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
661
- await state.set(key, next, await indexTtlMs(state, next, nowMs));
662
- });
663
- }
664
- async function removeFromIndex(state, key, subscriptionId, nowMs) {
665
- await withIndexLock(state, key, async () => {
666
- const existing = await readSubscriptionIdIndex(state, key);
667
- const next = existing.filter((id) => id !== subscriptionId);
668
- await state.set(key, next, await indexTtlMs(state, next, nowMs));
669
- });
670
- }
671
- async function indexTtlMs(state, subscriptionIds, nowMs) {
672
- const records = await Promise.all(
673
- subscriptionIds.map(
674
- async (id) => parseSubscription(await state.get(subscriptionKey(id)))
675
- )
676
- );
677
- const latestExpiresAtMs = Math.max(
678
- nowMs,
679
- ...records.filter(
680
- (record) => record !== void 0 && activeAt(record, nowMs)
681
- ).map((record) => record.expiresAtMs)
682
- );
683
- return ttlUntil(latestExpiresAtMs, nowMs);
684
- }
685
- function parseSubscription(value) {
686
- if (value === void 0 || value === null) {
687
- return void 0;
688
- }
689
- return subscriptionSchema.parse(value);
690
- }
691
- function activeAt(subscription, nowMs) {
692
- return subscription.status === "active" && subscription.expiresAtMs > nowMs;
693
- }
694
- function matchesEvent(subscription, input) {
695
- return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) && resourceEventMatches(subscription.match, input.data);
696
- }
697
- async function createResourceEventSubscription(input, options = {}) {
698
- const state = options.state ?? getStateAdapter();
699
- await state.connect();
700
- const nowMs = options.nowMs ?? Date.now();
701
- const events = [...new Set(input.events.map((event) => event.trim()))].filter(
702
- Boolean
703
- );
704
- if (events.length === 0) {
705
- throw new Error("Resource event subscription requires at least one event");
706
- }
707
- if (input.expiresAtMs <= nowMs) {
708
- throw new Error("Resource event subscription expiry must be in the future");
709
- }
710
- const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
711
- const id = buildSubscriptionId({
712
- conversationId: input.conversationId,
713
- events,
714
- match,
715
- namespace: input.namespace,
716
- identifier: input.identifier
717
- });
718
- const record = {
719
- conversationId: input.conversationId,
720
- createdAtMs: nowMs,
721
- events,
722
- expiresAtMs: input.expiresAtMs,
723
- id,
724
- intent: input.intent,
725
- label: input.label,
726
- ...match ? { match } : void 0,
727
- namespace: input.namespace,
728
- identifier: input.identifier,
729
- resourceType: input.resourceType,
730
- status: "active",
731
- updatedAtMs: nowMs
732
- };
733
- const parsed = subscriptionSchema.parse(record);
734
- await state.set(
735
- subscriptionKey(id),
736
- parsed,
737
- ttlUntil(parsed.expiresAtMs, nowMs)
738
- );
739
- await addToIndex(
740
- state,
741
- resourceIndexKey(input.namespace, input.identifier),
742
- id,
743
- nowMs
744
- );
745
- await addToIndex(
746
- state,
747
- conversationIndexKey(input.conversationId),
748
- id,
749
- nowMs
750
- );
751
- return parsed;
752
- }
753
- async function listResourceEventSubscriptions(input) {
754
- const state = input.state ?? getStateAdapter();
755
- await state.connect();
756
- const nowMs = input.nowMs ?? Date.now();
757
- const ids = await readSubscriptionIdIndex(
758
- state,
759
- conversationIndexKey(input.conversationId)
760
- );
761
- const records = await Promise.all(
762
- ids.map(
763
- async (id) => parseSubscription(await state.get(subscriptionKey(id)))
764
- )
765
- );
766
- return records.filter(
767
- (record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
768
- ).sort((left, right) => left.createdAtMs - right.createdAtMs);
769
- }
770
- async function cancelResourceEventSubscription(input) {
771
- const state = input.state ?? getStateAdapter();
772
- await state.connect();
773
- return await withSubscriptionLock(state, input.id, async () => {
774
- const current = parseSubscription(
775
- await state.get(subscriptionKey(input.id))
776
- );
777
- if (!current || current.conversationId !== input.conversationId) {
778
- return void 0;
779
- }
780
- const nowMs = input.nowMs ?? Date.now();
781
- const next = {
782
- ...current,
783
- status: "cancelled",
784
- updatedAtMs: nowMs
785
- };
786
- await state.set(
787
- subscriptionKey(input.id),
788
- next,
789
- JUNIOR_THREAD_STATE_TTL_MS
790
- );
791
- await removeFromIndex(
792
- state,
793
- resourceIndexKey(current.namespace, current.identifier),
794
- input.id,
795
- nowMs
796
- );
797
- await removeFromIndex(
798
- state,
799
- conversationIndexKey(current.conversationId),
800
- input.id,
801
- nowMs
802
- );
803
- return next;
804
- });
805
- }
806
- async function cancelSubscriptions(input) {
807
- const subscriptions = await listResourceEventSubscriptions(input);
808
- for (const subscription of subscriptions) {
809
- await cancelResourceEventSubscription({
810
- conversationId: input.conversationId,
811
- id: subscription.id,
812
- nowMs: input.nowMs,
813
- state: input.state
814
- });
815
- }
816
- }
817
- async function collectResourceEventMatchKeys(input) {
818
- const state = input.state ?? getStateAdapter();
819
- await state.connect();
820
- const nowMs = input.nowMs ?? Date.now();
821
- const eventTypes = new Set(
822
- input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean)
823
- );
824
- if (eventTypes.size === 0) return [];
825
- const keys = /* @__PURE__ */ new Set();
826
- const identifiers = new Set(
827
- input.identifiers.map((value) => value.trim()).filter(Boolean)
828
- );
829
- for (const identifier of identifiers) {
830
- const ids = await readSubscriptionIdIndex(
831
- state,
832
- resourceIndexKey(input.namespace, identifier)
833
- );
834
- const records = await Promise.all(
835
- ids.map(
836
- async (id) => parseSubscription(await state.get(subscriptionKey(id)))
837
- )
838
- );
839
- for (const record of records) {
840
- if (!record) continue;
841
- if (record.namespace !== input.namespace) continue;
842
- if (record.identifier !== identifier) continue;
843
- if (!activeAt(record, nowMs)) continue;
844
- if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
845
- for (const key of Object.keys(record.match ?? {})) {
846
- keys.add(key);
847
- }
848
- }
849
- }
850
- return [...keys].sort();
851
- }
852
- async function findMatchingResourceEventSubscriptions(input) {
853
- const state = input.state ?? getStateAdapter();
854
- await state.connect();
855
- const nowMs = input.nowMs ?? Date.now();
856
- const ids = await readSubscriptionIdIndex(
857
- state,
858
- resourceIndexKey(input.namespace, input.identifier)
859
- );
860
- const records = await Promise.all(
861
- ids.map(
862
- async (id) => parseSubscription(await state.get(subscriptionKey(id)))
863
- )
864
- );
865
- return records.filter(
866
- (record) => record !== void 0 && matchesEvent(record, {
867
- data: input.data,
868
- eventType: input.eventType,
869
- nowMs,
870
- namespace: input.namespace,
871
- identifier: input.identifier
872
- })
873
- );
874
- }
875
- async function deliverResourceEventSubscription(input) {
876
- const state = input.state ?? getStateAdapter();
877
- await state.connect();
878
- return await withSubscriptionLock(
879
- state,
880
- input.subscription.id,
881
- async () => {
882
- const nowMs = input.nowMs ?? Date.now();
883
- const current = parseSubscription(
884
- await state.get(subscriptionKey(input.subscription.id))
885
- );
886
- if (!current || !matchesEvent(current, {
887
- data: input.data,
888
- eventType: input.eventType,
889
- nowMs,
890
- namespace: input.namespace,
891
- identifier: input.identifier
892
- })) {
893
- return false;
894
- }
895
- const delivered = await input.deliver(current);
896
- if (input.terminal) {
897
- const latest = parseSubscription(
898
- await state.get(subscriptionKey(current.id))
899
- );
900
- if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) {
901
- return delivered;
902
- }
903
- const next = {
904
- ...current,
905
- status: "completed",
906
- updatedAtMs: nowMs
907
- };
908
- await state.set(
909
- subscriptionKey(current.id),
910
- next,
911
- JUNIOR_THREAD_STATE_TTL_MS
912
- );
913
- await removeFromIndex(
914
- state,
915
- resourceIndexKey(current.namespace, current.identifier),
916
- current.id,
917
- nowMs
918
- );
919
- await removeFromIndex(
920
- state,
921
- conversationIndexKey(current.conversationId),
922
- current.id,
923
- nowMs
924
- );
925
- }
926
- return delivered;
927
- },
928
- input.waitDeadlineMs
929
- );
930
- }
931
-
932
- // src/chat/resource-events/tool-support.ts
933
- var RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
934
- var RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
935
- var STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
936
- var RESOURCE_WATCH_TOOL_SOURCE = {
937
- id: "resource-watches",
938
- description: "Inspect or stop resource watches for the current conversation."
939
- };
940
-
941
- // src/chat/slack/tool-support/context.ts
942
- function getSlackToolContext(context) {
943
- const source = context.source.kind === "slack" ? context.source : void 0;
944
- const location = context.location?.provider === "slack" ? context.location : source ? {
945
- provider: "slack",
946
- teamId: source.teamId,
947
- channelId: source.channelId,
948
- threadTs: source.threadTs
949
- } : void 0;
950
- if (!location) return void 0;
951
- const destinationChannelId = parseSlackChannelReferenceId(
952
- context.destination.platform === "slack" ? context.destination.channelId : location.channelId
953
- );
954
- const locationChannelId = parseSlackChannelReferenceId(location.channelId);
955
- const teamId = parseSlackTeamId(location.teamId);
956
- if (!destinationChannelId || !locationChannelId || !teamId) {
957
- return void 0;
958
- }
959
- return {
960
- actor: context.actor?.platform === "slack" ? context.actor : void 0,
961
- destinationChannelId,
962
- locationChannelId,
963
- messageChannelId: source ? parseSlackChannelReferenceId(source.channelId) : void 0,
964
- messageTs: parseSlackMessageTs(source?.messageTs),
965
- teamId,
966
- threadTs: parseSlackMessageTs(location.threadTs)
967
- };
968
- }
969
-
970
- // src/chat/plugins/viewer.ts
971
- import { randomUUID } from "crypto";
972
- import { and as and3, asc, eq as eq3 } from "drizzle-orm";
973
- import {
974
- identitySchema,
975
- userSchema
976
- } from "@sentry/junior-plugin-api";
977
- function identityFromRow(row) {
978
- return identitySchema.parse({
979
- id: row.id,
980
- provider: row.provider,
981
- providerSubjectId: row.providerSubjectId,
982
- ...row.providerTenantId ? { providerTenantId: row.providerTenantId } : void 0,
983
- ...row.displayName ? { displayName: row.displayName } : void 0,
984
- ...row.handle ? { handle: row.handle } : void 0
985
- });
986
- }
987
- async function readUserById(db, userRow) {
988
- const identityRows = await db.select().from(juniorIdentities).where(
989
- and3(
990
- eq3(juniorIdentities.userId, userRow.id),
991
- eq3(juniorIdentities.kind, "user")
992
- )
993
- ).orderBy(
994
- asc(juniorIdentities.provider),
995
- asc(juniorIdentities.providerTenantId),
996
- asc(juniorIdentities.providerSubjectId)
997
- );
998
- return userSchema.parse({
999
- email: userRow.primaryEmail,
1000
- id: userRow.id,
1001
- identities: identityRows.map(identityFromRow),
1002
- ...userRow.displayName ? { displayName: userRow.displayName } : void 0
1003
- });
1004
- }
1005
- async function resolveViewerUserFromSql(db, email) {
1006
- const normalizedEmail = normalizeIdentityEmail(email);
1007
- if (!normalizedEmail) return void 0;
1008
- const now = /* @__PURE__ */ new Date();
1009
- const rows = await db.insert(juniorUsers).values({
1010
- id: randomUUID(),
1011
- primaryEmail: email.trim(),
1012
- primaryEmailNormalized: normalizedEmail,
1013
- displayName: null,
1014
- createdAt: now,
1015
- updatedAt: now
1016
- }).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning();
1017
- const userRow = rows[0] ?? (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
1018
- return userRow ? await readUserById(db, userRow) : void 0;
1019
- }
1020
- async function resolveViewerUser(email) {
1021
- return await resolveViewerUserFromSql(getDb(), email);
1022
- }
1023
- async function findUserByEmailFromSql(db, email) {
1024
- const normalizedEmail = normalizeIdentityEmail(email);
1025
- if (!normalizedEmail) return void 0;
1026
- const userRow = (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
1027
- return userRow ? await readUserById(db, userRow) : void 0;
1028
- }
1029
- async function findUserByEmail(email) {
1030
- return await findUserByEmailFromSql(getDb(), email);
1031
- }
1032
- async function updateViewerDisplayNameFromSql(db, userId, displayName) {
1033
- const rows = await db.update(juniorUsers).set({ displayName, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(juniorUsers.id, userId)).returning();
1034
- const userRow = rows[0];
1035
- return userRow ? await readUserById(db, userRow) : void 0;
1036
- }
1037
- async function updateViewerDisplayName(userId, displayName) {
1038
- return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
1039
- }
1040
- async function readActorIdentityFromSql(db, actor) {
1041
- if (actor.platform === "system") return void 0;
1042
- const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
1043
- const rows = await db.select({
1044
- identity: juniorIdentities,
1045
- user: juniorUsers
1046
- }).from(juniorIdentities).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
1047
- and3(
1048
- eq3(juniorIdentities.kind, "user"),
1049
- eq3(juniorIdentities.provider, actor.platform),
1050
- eq3(juniorIdentities.providerTenantId, providerTenantId),
1051
- eq3(juniorIdentities.providerSubjectId, actor.userId)
1052
- )
1053
- ).limit(1);
1054
- const row = rows[0];
1055
- if (!row) return void 0;
1056
- return {
1057
- identity: identityFromRow(row.identity),
1058
- ...row.user ? { user: await readUserById(db, row.user) } : void 0
1059
- };
1060
- }
1061
- async function readActorIdentity(actor) {
1062
- return await readActorIdentityFromSql(getDb(), actor);
1063
- }
1064
-
1065
- // src/chat/credentials/subject.ts
1066
- import { createHmac, timingSafeEqual } from "crypto";
1067
-
1068
- // src/chat/slack/client.ts
1069
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
1070
- import { WebClient } from "@slack/web-api";
1071
-
1072
- // src/chat/slack/workspace-context.ts
1073
- import { AsyncLocalStorage } from "async_hooks";
1074
- var workspaceTeamIdStorage = new AsyncLocalStorage();
1075
- function runWithWorkspaceTeamId(teamId, fn) {
1076
- if (!teamId) return fn();
1077
- return workspaceTeamIdStorage.run(teamId, fn);
1078
- }
1079
- function getWorkspaceTeamId() {
1080
- return workspaceTeamIdStorage.getStore();
1081
- }
1082
-
1083
- // src/chat/slack/client.ts
1084
- var SlackActionError = class extends Error {
1085
- code;
1086
- apiError;
1087
- needed;
1088
- provided;
1089
- statusCode;
1090
- requestId;
1091
- errorData;
1092
- retryAfterSeconds;
1093
- detail;
1094
- detailLine;
1095
- detailRule;
1096
- constructor(message, code, options = {}) {
1097
- super(message);
1098
- this.name = "SlackActionError";
1099
- this.code = code;
1100
- this.apiError = options.apiError;
1101
- this.needed = options.needed;
1102
- this.provided = options.provided;
1103
- this.statusCode = options.statusCode;
1104
- this.requestId = options.requestId;
1105
- this.errorData = options.errorData;
1106
- this.retryAfterSeconds = options.retryAfterSeconds;
1107
- this.detail = options.detail;
1108
- this.detailLine = options.detailLine;
1109
- this.detailRule = options.detailRule;
1110
- }
1111
- };
1112
- function serializeSlackErrorData(data) {
1113
- if (!data || typeof data !== "object") {
1114
- return void 0;
1115
- }
1116
- const filtered = Object.fromEntries(
1117
- Object.entries(data).filter(
1118
- ([key]) => key !== "error"
1119
- )
1120
- );
1121
- if (Object.keys(filtered).length === 0) {
1122
- return void 0;
1123
- }
1124
- try {
1125
- const serialized = JSON.stringify(filtered);
1126
- return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
1127
- } catch {
1128
- return void 0;
1129
- }
1130
- }
1131
- function getHeaderString(headers, name) {
1132
- if (!headers || typeof headers !== "object") {
1133
- return void 0;
1134
- }
1135
- const key = name.toLowerCase();
1136
- const entries = headers;
1137
- for (const [entryKey, value] of Object.entries(entries)) {
1138
- if (entryKey.toLowerCase() !== key) continue;
1139
- if (typeof value === "string") return value;
1140
- if (Array.isArray(value)) {
1141
- const first = value.find((entry) => typeof entry === "string");
1142
- return typeof first === "string" ? first : void 0;
1143
- }
1144
- }
1145
- return void 0;
1146
- }
1147
- function parseSlackCanvasDetail(detail) {
1148
- if (typeof detail !== "string") {
1149
- return {};
1150
- }
1151
- const trimmed = detail.trim();
1152
- if (!trimmed) {
1153
- return {};
1154
- }
1155
- const parsed = {
1156
- detail: trimmed
1157
- };
1158
- const lineMatch = trimmed.match(/line\s+(\d+):/i);
1159
- if (lineMatch) {
1160
- const line = Number.parseInt(lineMatch[1] ?? "", 10);
1161
- if (Number.isFinite(line)) {
1162
- parsed.detailLine = line;
1163
- }
1164
- }
1165
- if (/unsupported heading depth/i.test(trimmed)) {
1166
- parsed.detailRule = "unsupported_heading_depth";
1167
- }
1168
- return parsed;
1169
- }
1170
- var installationTokenStorage = new AsyncLocalStorage2();
1171
- var clientsByToken = /* @__PURE__ */ new Map();
1172
- function runWithSlackInstallationToken(token, fn) {
1173
- const trimmed = token.trim();
1174
- if (!trimmed) {
1175
- throw new SlackActionError(
1176
- "Slack installation token binding requires a non-empty token",
1177
- "missing_token"
1178
- );
1179
- }
1180
- return installationTokenStorage.run({ token: trimmed }, fn);
1181
- }
1182
- function resolveSlackToken() {
1183
- const ambientToken = installationTokenStorage.getStore()?.token;
1184
- if (ambientToken) {
1185
- return ambientToken;
1186
- }
1187
- const envToken = getSlackBotToken();
1188
- if (envToken) {
1189
- return envToken;
1190
- }
1191
- const teamId = getWorkspaceTeamId();
1192
- if (teamId) {
1193
- throw new SlackActionError(
1194
- `Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`,
1195
- "missing_token"
1196
- );
1197
- }
1198
- throw new SlackActionError(
1199
- "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service",
1200
- "missing_token"
1201
- );
1202
- }
1203
- function normalizeSlackConversationId(channelId) {
1204
- return parseSlackChannelReferenceId(channelId);
1205
- }
1206
- function getClient() {
1207
- const token = resolveSlackToken();
1208
- let cached = clientsByToken.get(token);
1209
- if (!cached) {
1210
- cached = new WebClient(token, {
1211
- retryConfig: { retries: 0 },
1212
- rejectRateLimitedCalls: true
1213
- });
1214
- clientsByToken.set(token, cached);
1215
- }
1216
- return cached;
1217
- }
1218
- function mapSlackError(error) {
1219
- if (error instanceof SlackActionError) {
1220
- return error;
1221
- }
1222
- const candidate = error;
1223
- const apiError = candidate.data?.error;
1224
- const message = candidate.message ?? "Slack action failed";
1225
- const baseOptions = {
1226
- apiError,
1227
- statusCode: candidate.statusCode,
1228
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
1229
- errorData: serializeSlackErrorData(candidate.data),
1230
- ...parseSlackCanvasDetail(candidate.data?.detail)
1231
- };
1232
- if (apiError === "missing_scope") {
1233
- return new SlackActionError(message, "missing_scope", {
1234
- ...baseOptions,
1235
- needed: candidate.data?.needed,
1236
- provided: candidate.data?.provided
1237
- });
1238
- }
1239
- if (apiError === "not_in_channel") {
1240
- return new SlackActionError(message, "not_in_channel", baseOptions);
1241
- }
1242
- if (apiError === "restricted_action_read_only_channel") {
1243
- return new SlackActionError(message, "read_only_channel", baseOptions);
1244
- }
1245
- if (apiError === "already_reacted") {
1246
- return new SlackActionError(message, "already_reacted", baseOptions);
1247
- }
1248
- if (apiError === "no_reaction") {
1249
- return new SlackActionError(message, "no_reaction", baseOptions);
1250
- }
1251
- if (apiError === "invalid_arguments") {
1252
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1253
- }
1254
- if (apiError === "invalid_cursor") {
1255
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1256
- }
1257
- if (apiError === "invalid_name") {
1258
- return new SlackActionError(message, "invalid_arguments", baseOptions);
1259
- }
1260
- if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
1261
- return new SlackActionError(message, "not_found", baseOptions);
1262
- }
1263
- if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
1264
- return new SlackActionError(message, "feature_unavailable", baseOptions);
1265
- }
1266
- if (apiError === "canvas_creation_failed") {
1267
- return new SlackActionError(message, "canvas_creation_failed", baseOptions);
1268
- }
1269
- if (apiError === "canvas_editing_failed") {
1270
- return new SlackActionError(message, "canvas_editing_failed", baseOptions);
1271
- }
1272
- if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
1273
- return new SlackActionError(message, "rate_limited", {
1274
- ...baseOptions,
1275
- retryAfterSeconds: candidate.retryAfter
1276
- });
1277
- }
1278
- return new SlackActionError(message, "internal_error", baseOptions);
1279
- }
1280
- var MAX_RETRY_DELAY_MS = 1e4;
1281
- var MAX_TOTAL_RETRY_DELAY_MS = 2e4;
1282
- var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
1283
- "ECONNREFUSED",
1284
- "ECONNRESET",
1285
- "ENOTFOUND",
1286
- "EAI_AGAIN"
1287
- ]);
1288
- var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
1289
- "ETIMEDOUT",
1290
- "ECONNABORTED",
1291
- "ESOCKETTIMEDOUT"
1292
- ]);
1293
- function findNetworkErrorCode(error, depth = 0) {
1294
- if (!error || typeof error !== "object" || depth > 4) {
1295
- return void 0;
1296
- }
1297
- const candidate = error;
1298
- if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) {
1299
- return candidate.code;
1300
- }
1301
- return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
1302
- }
1303
- function hasSocketHangUpMessage(error) {
1304
- return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
1305
- }
1306
- function classifySlackRetry(raw, mapped) {
1307
- if (mapped.code === "rate_limited") {
1308
- return "rate_limited";
1309
- }
1310
- if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) {
1311
- return "server_error";
1312
- }
1313
- const networkCode = findNetworkErrorCode(raw);
1314
- if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) {
1315
- return "connection";
1316
- }
1317
- if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) {
1318
- return "timeout";
1319
- }
1320
- if (hasSocketHangUpMessage(raw)) {
1321
- return "connection";
1322
- }
1323
- return "none";
1324
- }
1325
- async function withSlackRetries(task, maxAttempts = 3, context = {}) {
1326
- let attempt = 0;
1327
- let totalDelayMs = 0;
1328
- const action = context.action ?? "unknown";
1329
- while (attempt < maxAttempts) {
1330
- attempt += 1;
1331
- const attemptNumber = attempt;
1332
- try {
1333
- return await withSpan(
1334
- `POST slack.com/api/${action}`,
1335
- "http.client",
1336
- {},
1337
- async () => {
1338
- try {
1339
- return await task();
1340
- } catch (error) {
1341
- const mapped = mapSlackError(error);
1342
- const errorAttrs = {
1343
- "error.type": mapped.code
1344
- };
1345
- if (mapped.apiError) {
1346
- errorAttrs["app.slack.api_error_code"] = mapped.apiError;
1347
- }
1348
- if (mapped.code === "rate_limited") {
1349
- errorAttrs["http.response.status_code"] = 429;
1350
- if (mapped.retryAfterSeconds) {
1351
- errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
1352
- }
1353
- } else if (mapped.statusCode != null) {
1354
- errorAttrs["http.response.status_code"] = mapped.statusCode;
1355
- }
1356
- setSpanAttributes(errorAttrs);
1357
- setSpanStatus("error");
1358
- throw error;
1359
- }
1360
- },
1361
- {
1362
- "http.request.method": "POST",
1363
- "server.address": "slack.com",
1364
- "url.scheme": "https",
1365
- "url.path": `/api/${action}`,
1366
- "app.slack.method": action,
1367
- "app.retry.max_attempts": maxAttempts,
1368
- ...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
1369
- ...context.attributes ?? {},
1370
- ...context.spanAttributes ?? {}
1371
- }
1372
- );
1373
- } catch (error) {
1374
- const mapped = mapSlackError(error);
1375
- const retryClass = classifySlackRetry(error, mapped);
1376
- const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
1377
- const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
1378
- const baseLogAttributes = {
1379
- "app.slack.action": action,
1380
- "app.slack.error_code": mapped.code,
1381
- ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
1382
- ...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
1383
- ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
1384
- ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
1385
- ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
1386
- ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
1387
- ...context.attributes ?? {}
1388
- };
1389
- if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
1390
- logWarn("slack.action.failed", {
1391
- ...baseLogAttributes,
1392
- ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
1393
- });
1394
- throw mapped;
1395
- }
1396
- logWarn("slack.action.retrying", {
1397
- ...baseLogAttributes,
1398
- "app.slack.retry_attempt": attempt,
1399
- "app.slack.retry_class": retryClass
1400
- });
1401
- const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
1402
- const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
1403
- const delayMs = Math.min(
1404
- retryAfterMs ?? backoffMs,
1405
- MAX_RETRY_DELAY_MS,
1406
- remainingDelayBudgetMs
1407
- );
1408
- totalDelayMs += delayMs;
1409
- await sleep(delayMs);
1410
- }
1411
- }
1412
- throw new SlackActionError(
1413
- "Slack action exhausted retries",
1414
- "internal_error"
1415
- );
1416
- }
1417
- function getSlackClient() {
1418
- return getClient();
1419
- }
1420
- function isDmChannel(channelId) {
1421
- const normalized = normalizeSlackConversationId(channelId);
1422
- return Boolean(normalized && normalized.startsWith("D"));
1423
- }
1424
- function isConversationScopedChannel(channelId) {
1425
- const normalized = normalizeSlackConversationId(channelId);
1426
- if (!normalized) return false;
1427
- return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
1428
- }
1429
- function isConversationChannel(channelId) {
1430
- const normalized = normalizeSlackConversationId(channelId);
1431
- if (!normalized) return false;
1432
- return normalized.startsWith("C") || normalized.startsWith("G");
1433
- }
1434
- async function getFilePermalink(fileId) {
1435
- const client = getClient();
1436
- const response = await withSlackRetries(
1437
- () => client.files.info({
1438
- file: fileId
1439
- }),
1440
- 3,
1441
- {
1442
- action: "files.info",
1443
- idempotent: true,
1444
- spanAttributes: { "app.slack.file_id": fileId }
1445
- }
1446
- );
1447
- return response.file?.permalink;
1448
- }
1449
- async function downloadPrivateSlackFile(url) {
1450
- const token = resolveSlackToken();
1451
- return withSpan(
1452
- "GET files.slack.com",
1453
- "http.client",
1454
- {},
1455
- async () => {
1456
- const response = await fetch(url, {
1457
- headers: {
1458
- Authorization: `Bearer ${token}`
1459
- }
1460
- });
1461
- setSpanAttributes({ "http.response.status_code": response.status });
1462
- if (!response.ok) {
1463
- setSpanAttributes({ "error.type": String(response.status) });
1464
- setSpanStatus("error");
1465
- throw new Error(`Slack file download failed: ${response.status}`);
1466
- }
1467
- return Buffer.from(await response.arrayBuffer());
1468
- },
1469
- {
1470
- "http.request.method": "GET",
1471
- "server.address": "files.slack.com",
1472
- "url.scheme": "https",
1473
- "app.slack.method": "files.download"
1474
- }
1475
- );
1476
- }
1477
-
1478
- // src/chat/credentials/subject.ts
1479
- var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
1480
- var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
1481
- function getCredentialSubjectSecret() {
1482
- return process.env.JUNIOR_SECRET?.trim() || void 0;
1483
- }
1484
- function buildPayload(parts) {
1485
- return [CREDENTIAL_SUBJECT_HMAC_CONTEXT, ...parts].join("\0");
1486
- }
1487
- function signPayload(secret, payload) {
1488
- const digest2 = createHmac("sha256", secret).update(payload).digest("hex");
1489
- return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest2}`;
1490
- }
1491
- function timingSafeMatch(expected, actual) {
1492
- const expectedBuffer = Buffer.from(expected);
1493
- const actualBuffer = Buffer.from(actual);
1494
- if (expectedBuffer.length !== actualBuffer.length) {
1495
- return false;
1496
- }
1497
- return timingSafeEqual(expectedBuffer, actualBuffer);
1498
- }
1499
- function createSlackDirectCredentialSubject(input) {
1500
- const channelId = normalizeSlackConversationId(input.channelId);
1501
- const teamId = input.teamId?.trim();
1502
- const userId = parseActorUserId(input.userId);
1503
- if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
1504
- return void 0;
1505
- }
1506
- return {
1507
- type: "user",
1508
- userId,
1509
- allowedWhen: "private-direct-conversation"
1510
- };
1511
- }
1512
- function bindSlackDirectCredentialSubject(input) {
1513
- const channelId = normalizeSlackConversationId(input.channelId);
1514
- const teamId = input.teamId.trim();
1515
- const secret = getCredentialSubjectSecret();
1516
- const { subject } = input;
1517
- const userId = parseActorUserId(subject.userId);
1518
- if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
1519
- return void 0;
1520
- }
1521
- return {
1522
- type: "user",
1523
- userId,
1524
- allowedWhen: subject.allowedWhen,
1525
- binding: {
1526
- type: "slack-direct-conversation",
1527
- teamId,
1528
- channelId,
1529
- signature: signPayload(
1530
- secret,
1531
- buildPayload([subject.allowedWhen, teamId, channelId, userId])
1532
- )
1533
- }
1534
- };
1535
- }
1536
- function verifySlackDirectCredentialSubject(input) {
1537
- const channelId = normalizeSlackConversationId(input.channelId);
1538
- const secret = getCredentialSubjectSecret();
1539
- if (!channelId || !secret) {
1540
- return false;
1541
- }
1542
- const { subject } = input;
1543
- const binding = subject.binding;
1544
- 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) {
1545
- return false;
1546
- }
1547
- const expected = signPayload(
1548
- secret,
1549
- buildPayload([
1550
- subject.allowedWhen,
1551
- binding.teamId,
1552
- binding.channelId,
1553
- subject.userId
1554
- ])
1555
- );
1556
- return timingSafeMatch(expected, binding.signature);
1557
- }
1558
- function bindScheduledTaskCredentialSubject(input) {
1559
- const secret = getCredentialSubjectSecret();
1560
- const plugin = input.plugin.trim();
1561
- const userId = parseActorUserId(input.subject.userId);
1562
- if (!secret || plugin !== "scheduler" || !userId || input.subject.allowedWhen !== "scheduled-task") {
1563
- return void 0;
1564
- }
1565
- const taskId = input.subject.taskId;
1566
- if (!taskId || taskId !== taskId.trim()) {
1567
- return void 0;
1568
- }
1569
- return {
1570
- type: "user",
1571
- userId,
1572
- allowedWhen: "scheduled-task",
1573
- taskId,
1574
- binding: {
1575
- type: "scheduled-task",
1576
- plugin,
1577
- taskId,
1578
- signature: signPayload(
1579
- secret,
1580
- buildPayload(["scheduled-task", plugin, taskId, userId])
1581
- )
1582
- }
1583
- };
1584
- }
1585
- function verifyScheduledTaskCredentialSubject(input) {
1586
- const secret = getCredentialSubjectSecret();
1587
- const { subject } = input;
1588
- const binding = subject.binding;
1589
- if (!secret || input.plugin !== "scheduler" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "scheduled-task" || !subject.taskId || binding.type !== "scheduled-task" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) {
1590
- return false;
1591
- }
1592
- const expected = signPayload(
1593
- secret,
1594
- buildPayload([
1595
- "scheduled-task",
1596
- binding.plugin,
1597
- binding.taskId,
1598
- subject.userId
1599
- ])
1600
- );
1601
- return timingSafeMatch(expected, binding.signature);
1602
- }
1603
- function bindEventTaskCredentialSubject(input) {
1604
- const secret = getCredentialSubjectSecret();
1605
- const plugin = input.plugin.trim();
1606
- const userId = parseActorUserId(input.subject.userId);
1607
- if (!secret || plugin !== "junior" || !userId || input.subject.allowedWhen !== "event-task") {
1608
- return void 0;
1609
- }
1610
- const taskId = input.subject.taskId;
1611
- if (!taskId || taskId !== taskId.trim()) {
1612
- return void 0;
1613
- }
1614
- return {
1615
- type: "user",
1616
- userId,
1617
- allowedWhen: "event-task",
1618
- taskId,
1619
- binding: {
1620
- type: "event-task",
1621
- plugin,
1622
- taskId,
1623
- signature: signPayload(
1624
- secret,
1625
- buildPayload(["event-task", plugin, taskId, userId])
1626
- )
1627
- }
1628
- };
1629
- }
1630
- function verifyEventTaskCredentialSubject(input) {
1631
- const secret = getCredentialSubjectSecret();
1632
- const { subject } = input;
1633
- const binding = subject.binding;
1634
- if (!secret || input.plugin !== "junior" || subject.type !== "user" || !isActorUserId(subject.userId) || subject.allowedWhen !== "event-task" || !subject.taskId || binding.type !== "event-task" || binding.plugin !== input.plugin || binding.taskId !== subject.taskId) {
1635
- return false;
1636
- }
1637
- const expected = signPayload(
1638
- secret,
1639
- buildPayload([
1640
- "event-task",
1641
- binding.plugin,
1642
- binding.taskId,
1643
- subject.userId
1644
- ])
1645
- );
1646
- return timingSafeMatch(expected, binding.signature);
1647
- }
1648
-
1649
- // src/chat/slack/tool-support/channel-capabilities.ts
1650
- function resolveChannelCapabilities(channelId) {
1651
- return {
1652
- canCreateCanvas: isConversationScopedChannel(channelId),
1653
- canSendFiles: isConversationScopedChannel(channelId),
1654
- canPostToChannel: isConversationChannel(channelId),
1655
- canAddReactions: isConversationScopedChannel(channelId)
1656
- };
1657
- }
1658
-
1659
- // src/chat/plugins/agent-hooks.ts
1660
- import { z as z3 } from "zod";
1661
-
1662
- // src/chat/workspaces/checkout-path.ts
1663
- function tryWorkspaceRepoCheckoutPath(repo) {
1664
- const name = repo.split("/").filter(Boolean).at(-1);
1665
- if (!name || name === "." || name === ".." || !/^[A-Za-z0-9._-]+$/.test(name)) {
1666
- return void 0;
1667
- }
1668
- return `repos/${name}`;
1669
- }
1670
- function workspaceRepoCheckoutPath(repo) {
1671
- const path = tryWorkspaceRepoCheckoutPath(repo);
1672
- if (!path) {
1673
- throw new Error(`Invalid repository name for checkout path: ${repo}`);
1674
- }
1675
- return path;
1676
- }
1677
-
1678
- // src/chat/workspaces/store.ts
1679
- import { randomUUID as randomUUID2 } from "crypto";
1680
- import { and as and5, asc as asc3, eq as eq5, sql as sql2 } from "drizzle-orm";
1681
-
1682
- // src/chat/sandbox/snapshot/builder-sandbox.ts
1683
- import { Sandbox } from "@vercel/sandbox";
1684
- async function getWorkspaceSnapshotBuilder(name, signal) {
1685
- const credentials = getVercelSandboxCredentials();
1686
- return await Sandbox.get({
1687
- name,
1688
- resume: true,
1689
- signal,
1690
- ...credentials ?? {}
1691
- });
1692
- }
1693
- async function deleteWorkspaceSnapshotBuilders(names) {
1694
- const credentials = getVercelSandboxCredentials();
1695
- let hasError = false;
1696
- let firstError;
1697
- for (const name of new Set(names)) {
1698
- try {
1699
- const sandbox = await Sandbox.get({
1700
- name,
1701
- resume: false,
1702
- ...credentials ?? {}
1703
- });
1704
- await sandbox.delete();
1705
- } catch (error) {
1706
- if (!isSandboxMissingError(error)) {
1707
- if (!hasError) firstError = error;
1708
- hasError = true;
1709
- }
1710
- }
1711
- }
1712
- if (hasError) throw firstError;
1713
- }
1714
-
1715
- // src/chat/sandbox/snapshot/runtime.ts
1716
- var SANDBOX_RUNTIME = "node22";
1717
-
1718
- // src/chat/sandbox/snapshot/store.ts
1719
- import { and as and4, asc as asc2, desc as desc2, eq as eq4, ne, or } from "drizzle-orm";
1720
- function builderNames(rows) {
1721
- return [
1722
- ...new Set(
1723
- rows.map((row) => row.sandboxName).filter((name) => Boolean(name))
1724
- )
1725
- ];
1726
- }
1727
- function sameWorkspaceRecipe(stored, expected) {
1728
- if (stored.setupScript !== expected.setupScript || stored.repos.length !== expected.repos.length) {
1729
- return false;
1730
- }
1731
- const expectedRepos = new Set(
1732
- expected.repos.map((repo) => JSON.stringify([repo.provider, repo.repo]))
1733
- );
1734
- return stored.repos.every(
1735
- (repo) => expectedRepos.has(JSON.stringify([repo.provider, repo.repo]))
1736
- );
1737
- }
1738
- function snapshotFromRow(row) {
1739
- if (row.status !== "ready" || !row.snapshotId || !row.generatedAt || row.buildDurationMs == null) {
1740
- throw new Error(`Invalid ready Workspace snapshot row: ${row.id}`);
1741
- }
1742
- return {
1743
- id: row.snapshotId,
1744
- generatedAt: row.generatedAt,
1745
- buildDurationMs: row.buildDurationMs,
1746
- sizeBytes: row.sizeBytes,
1747
- profileHash: row.profileHash
1748
- };
1749
- }
1750
- function snapshotBuildFromRow(row) {
1751
- if (row.status === "ready" || !row.buildStartedAt || !row.buildPhase) {
1752
- throw new Error(`Invalid Workspace snapshot build row: ${row.id}`);
1753
- }
1754
- return {
1755
- id: row.id,
1756
- status: row.status,
1757
- phase: row.buildPhase,
1758
- profileHash: row.profileHash,
1759
- startedAt: row.buildStartedAt,
1760
- sandboxName: row.buildSandboxName,
1761
- commandId: row.buildCommandId,
1762
- error: row.buildError
1763
- };
1764
- }
1765
- async function loadSnapshotsForProfile(db, workspaceId, profileHash) {
1766
- const profile = and4(
1767
- eq4(juniorSnapshots.workspaceId, workspaceId),
1768
- eq4(juniorSnapshots.profileHash, profileHash)
1769
- );
1770
- const order = [
1771
- desc2(juniorSnapshots.updatedAt),
1772
- desc2(juniorSnapshots.createdAt),
1773
- desc2(juniorSnapshots.id)
1774
- ];
1775
- const [readyRows, buildRows] = await Promise.all([
1776
- db.select().from(juniorSnapshots).where(and4(profile, eq4(juniorSnapshots.status, "ready"))).orderBy(...order).limit(1),
1777
- db.select().from(juniorSnapshots).where(and4(profile, ne(juniorSnapshots.status, "ready"))).orderBy(asc2(juniorSnapshots.status), ...order).limit(1)
1778
- ]);
1779
- return {
1780
- ready: readyRows[0] ? snapshotFromRow(readyRows[0]) : null,
1781
- build: buildRows[0] ? snapshotBuildFromRow(buildRows[0]) : null
1782
- };
1783
- }
1784
- async function clearWorkspaceSnapshots(db, workspaceId) {
1785
- await db.select({ id: juniorWorkspaces.id }).from(juniorWorkspaces).where(eq4(juniorWorkspaces.id, workspaceId)).for("update");
1786
- const deleted = await db.delete(juniorSnapshots).where(eq4(juniorSnapshots.workspaceId, workspaceId)).returning({ sandboxName: juniorSnapshots.buildSandboxName });
1787
- return builderNames(deleted);
1788
- }
1789
- async function invalidateReadySnapshot(params) {
1790
- const executor = getSqlExecutor();
1791
- return await executor.transaction(async () => {
1792
- const db = executor.db();
1793
- const deleted = await db.delete(juniorSnapshots).where(
1794
- and4(
1795
- eq4(juniorSnapshots.workspaceId, params.workspaceId),
1796
- eq4(juniorSnapshots.profileHash, params.profileHash),
1797
- eq4(juniorSnapshots.status, "ready"),
1798
- eq4(juniorSnapshots.snapshotId, params.snapshotId)
1799
- )
1800
- ).returning({ sandboxName: juniorSnapshots.buildSandboxName });
1801
- return deleted[0]?.sandboxName ?? null;
1802
- });
1803
- }
1804
- async function setWorkspaceSnapshot(workspaceId, snapshot, options) {
1805
- const executor = getSqlExecutor();
1806
- return await executor.transaction(async () => {
1807
- const db = executor.db();
1808
- const now = /* @__PURE__ */ new Date();
1809
- const updated = await db.update(juniorSnapshots).set({
1810
- status: "ready",
1811
- snapshotId: snapshot.id,
1812
- buildDurationMs: snapshot.buildDurationMs,
1813
- sizeBytes: snapshot.sizeBytes,
1814
- generatedAt: snapshot.generatedAt,
1815
- buildStartedAt: null,
1816
- buildPhase: null,
1817
- buildCommandId: null,
1818
- buildError: null,
1819
- updatedAt: now
1820
- }).where(
1821
- and4(
1822
- eq4(juniorSnapshots.id, options.buildId),
1823
- eq4(juniorSnapshots.workspaceId, workspaceId),
1824
- eq4(juniorSnapshots.profileHash, snapshot.profileHash),
1825
- eq4(juniorSnapshots.status, "building")
1826
- )
1827
- ).returning({ id: juniorSnapshots.id });
1828
- if (updated.length === 0) {
1829
- return { written: false, replacedBuilderNames: [] };
1830
- }
1831
- const replaced = await db.delete(juniorSnapshots).where(
1832
- and4(
1833
- eq4(juniorSnapshots.workspaceId, workspaceId),
1834
- eq4(juniorSnapshots.profileHash, snapshot.profileHash),
1835
- ne(juniorSnapshots.id, options.buildId),
1836
- or(
1837
- eq4(juniorSnapshots.status, "failed"),
1838
- eq4(juniorSnapshots.status, "ready")
1839
- )
1840
- )
1841
- ).returning({ sandboxName: juniorSnapshots.buildSandboxName });
1842
- return {
1843
- written: true,
1844
- replacedBuilderNames: builderNames(replaced)
1845
- };
1846
- });
1847
- }
1848
- async function setWorkspaceSnapshotBuild(workspaceId, build, options = {}) {
1849
- const executor = getSqlExecutor();
1850
- return await executor.transaction(async () => {
1851
- const db = executor.db();
1852
- const now = /* @__PURE__ */ new Date();
1853
- const updated = await db.update(juniorSnapshots).set({
1854
- status: build.status,
1855
- buildStartedAt: build.startedAt,
1856
- buildPhase: build.phase,
1857
- buildSandboxName: build.sandboxName,
1858
- buildCommandId: build.commandId,
1859
- buildError: build.error,
1860
- updatedAt: now
1861
- }).where(
1862
- and4(
1863
- eq4(juniorSnapshots.id, build.id),
1864
- eq4(juniorSnapshots.workspaceId, workspaceId),
1865
- eq4(juniorSnapshots.profileHash, build.profileHash),
1866
- eq4(juniorSnapshots.status, "building")
1867
- )
1868
- ).returning({ id: juniorSnapshots.id });
1869
- if (updated.length > 0) {
1870
- return true;
1871
- }
1872
- if (!options.insertIfMissing) return false;
1873
- const workspaceRows = await db.select({ setupScript: juniorWorkspaces.setupScript }).from(juniorWorkspaces).where(eq4(juniorWorkspaces.id, workspaceId)).for("update");
1874
- const workspace = workspaceRows[0];
1875
- if (!workspace) return false;
1876
- if (options.expectedRecipe) {
1877
- const repos = await db.select({
1878
- provider: juniorWorkspaceRepos.provider,
1879
- repo: juniorWorkspaceRepos.repo
1880
- }).from(juniorWorkspaceRepos).where(eq4(juniorWorkspaceRepos.workspaceId, workspaceId));
1881
- if (!sameWorkspaceRecipe({ ...workspace, repos }, options.expectedRecipe)) {
1882
- return false;
1883
- }
1884
- }
1885
- const inserted = await db.insert(juniorSnapshots).values({
1886
- id: build.id,
1887
- workspaceId,
1888
- profileHash: build.profileHash,
1889
- status: build.status,
1890
- snapshotId: null,
1891
- buildDurationMs: null,
1892
- sizeBytes: null,
1893
- generatedAt: null,
1894
- buildStartedAt: build.startedAt,
1895
- buildPhase: build.phase,
1896
- buildSandboxName: build.sandboxName,
1897
- buildCommandId: build.commandId,
1898
- buildError: build.error,
1899
- createdAt: now,
1900
- updatedAt: now
1901
- }).onConflictDoNothing().returning({ id: juniorSnapshots.id });
1902
- return inserted.length > 0;
1903
- });
1904
- }
1905
-
1906
- // src/chat/workspaces/validation.ts
1907
- var WORKSPACE_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
1908
- var PROVIDER_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
1909
- var REPO_PATTERN = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
1910
- var MAX_SETUP_SCRIPT_BYTES = 64 * 1024;
1911
- var MAX_REPOS = 32;
1912
- var WorkspaceValidationError = class extends Error {
1913
- constructor(message) {
1914
- super(message);
1915
- this.name = "WorkspaceValidationError";
1916
- }
1917
- };
1918
- function normalizeWorkspaceRecipe(input) {
1919
- const name = input.name.trim().toLowerCase();
1920
- if (!WORKSPACE_NAME_PATTERN.test(name)) {
1921
- throw new WorkspaceValidationError(
1922
- "Workspace name must start with a letter and use only lowercase letters, digits, underscores, or hyphens."
1923
- );
1924
- }
1925
- const setupScript = input.setupScript ?? "";
1926
- if (typeof setupScript !== "string") {
1927
- throw new WorkspaceValidationError("Setup script must be a string.");
1928
- }
1929
- if (Buffer.byteLength(setupScript, "utf8") > MAX_SETUP_SCRIPT_BYTES) {
1930
- throw new WorkspaceValidationError(
1931
- `Setup script must be at most ${MAX_SETUP_SCRIPT_BYTES} bytes.`
1932
- );
1933
- }
1934
- if (!Array.isArray(input.repos)) {
1935
- throw new WorkspaceValidationError("Repositories must be an array.");
1936
- }
1937
- if (input.repos.length > MAX_REPOS) {
1938
- throw new WorkspaceValidationError(
1939
- `A Workspace can include at most ${MAX_REPOS} repositories.`
1940
- );
1941
- }
1942
- const repos = input.repos.map((entry, index) => {
1943
- const provider = entry.provider.trim().toLowerCase();
1944
- if (!PROVIDER_PATTERN.test(provider)) {
1945
- throw new WorkspaceValidationError(
1946
- `Repository ${index + 1} provider is invalid.`
1947
- );
1948
- }
1949
- const repo = entry.repo.trim();
1950
- if (!REPO_PATTERN.test(repo)) {
1951
- throw new WorkspaceValidationError(
1952
- `Repository ${index + 1} must use owner/name format.`
1953
- );
1954
- }
1955
- if (!tryWorkspaceRepoCheckoutPath(repo)) {
1956
- throw new WorkspaceValidationError(
1957
- `Repository ${index + 1} name cannot be used as a checkout path.`
1958
- );
1959
- }
1960
- return {
1961
- provider,
1962
- repo
1963
- };
1964
- });
1965
- const identities = /* @__PURE__ */ new Set();
1966
- for (const repo of repos) {
1967
- const identity = `${repo.provider}:${repo.repo.toLowerCase()}`;
1968
- if (identities.has(identity)) {
1969
- throw new WorkspaceValidationError(
1970
- `Duplicate repository in Workspace: ${repo.provider}:${repo.repo}`
1971
- );
1972
- }
1973
- identities.add(identity);
1974
- }
1975
- const checkoutPaths = /* @__PURE__ */ new Set();
1976
- for (const repo of repos) {
1977
- const path = tryWorkspaceRepoCheckoutPath(repo.repo);
1978
- const key = path.toLowerCase();
1979
- if (checkoutPaths.has(key)) {
1980
- throw new WorkspaceValidationError(
1981
- `Workspace checkout path collision: ${path}`
1982
- );
1983
- }
1984
- checkoutPaths.add(key);
1985
- }
1986
- return { name, setupScript, repos };
1987
- }
1988
-
1989
- // src/chat/workspaces/store.ts
1990
- function workspaceFromRows(row, repos, snapshot) {
1991
- return {
1992
- id: row.id,
1993
- name: row.name,
1994
- setupScript: row.setupScript,
1995
- repos: repos.map((repo) => ({
1996
- provider: repo.provider,
1997
- repo: repo.repo
1998
- })),
1999
- snapshot
2000
- };
2001
- }
2002
- async function workspaceWithSnapshot(db, row, repos) {
2003
- const workspace = workspaceFromRows(row, repos, null);
2004
- const profileHash = hash(SANDBOX_RUNTIME, workspace);
2005
- if (!profileHash) return workspace;
2006
- const snapshots = await loadSnapshotsForProfile(
2007
- db,
2008
- workspace.id,
2009
- profileHash
2010
- );
2011
- return { ...workspace, snapshot: snapshots.ready };
2012
- }
2013
- async function loadWorkspaceRepos(db, workspaceId) {
2014
- return await db.select().from(juniorWorkspaceRepos).where(eq5(juniorWorkspaceRepos.workspaceId, workspaceId)).orderBy(
2015
- asc3(juniorWorkspaceRepos.provider),
2016
- asc3(juniorWorkspaceRepos.repo)
2017
- );
2018
- }
2019
- async function replaceWorkspaceRepos(db, workspaceId, repos) {
2020
- await db.delete(juniorWorkspaceRepos).where(eq5(juniorWorkspaceRepos.workspaceId, workspaceId));
2021
- if (repos.length === 0) return;
2022
- await db.insert(juniorWorkspaceRepos).values(
2023
- repos.map((repo) => ({
2024
- workspaceId,
2025
- provider: repo.provider,
2026
- repo: repo.repo
2027
- }))
2028
- );
2029
- }
2030
- function sameWorkspaceRepos(current, next) {
2031
- if (current.length !== next.length) return false;
2032
- const identity = (repo) => `${repo.provider}:${repo.repo.toLowerCase()}`;
2033
- const currentIds = current.map(identity).sort();
2034
- const nextIds = next.map(identity).sort();
2035
- return currentIds.every((value, index) => value === nextIds[index]);
2036
- }
2037
- function isUniqueViolation(error) {
2038
- let current = error;
2039
- for (let depth = 0; current && depth < 6; depth += 1) {
2040
- if (typeof current === "object" && current !== null && "code" in current && current.code === "23505") {
2041
- return true;
2042
- }
2043
- current = typeof current === "object" && current !== null && "cause" in current ? current.cause : void 0;
2044
- }
2045
- return false;
2046
- }
2047
- async function cleanupSnapshotBuilders(workspaceId, names) {
2048
- if (names.length === 0) return;
2049
- try {
2050
- await deleteWorkspaceSnapshotBuilders(names);
2051
- } catch (error) {
2052
- logException(error, "sandbox.workspace_snapshot.builder.delete_failed", {
2053
- "app.workspace.id": workspaceId,
2054
- "app.sandbox.snapshot.builder_count": names.length
2055
- });
2056
- }
2057
- }
2058
- async function listWorkspaces(db) {
2059
- const [workspaces, repos] = await Promise.all([
2060
- db.select().from(juniorWorkspaces).orderBy(asc3(juniorWorkspaces.name)),
2061
- db.select().from(juniorWorkspaceRepos).orderBy(
2062
- asc3(juniorWorkspaceRepos.workspaceId),
2063
- asc3(juniorWorkspaceRepos.provider),
2064
- asc3(juniorWorkspaceRepos.repo)
2065
- )
2066
- ]);
2067
- return workspaces.map(
2068
- (workspace) => workspaceFromRows(
2069
- workspace,
2070
- repos.filter((repo) => repo.workspaceId === workspace.id),
2071
- null
2072
- )
2073
- );
2074
- }
2075
- async function listWorkspaceNamesByRepository(db, input) {
2076
- const provider = input.provider.trim().toLowerCase();
2077
- const repo = input.repo.trim();
2078
- const rows = await db.select({ name: juniorWorkspaces.name }).from(juniorWorkspaceRepos).innerJoin(
2079
- juniorWorkspaces,
2080
- eq5(juniorWorkspaces.id, juniorWorkspaceRepos.workspaceId)
2081
- ).where(
2082
- and5(
2083
- eq5(juniorWorkspaceRepos.provider, provider),
2084
- // Recipes store the provided repo casing; identity is case-insensitive.
2085
- sql2`lower(${juniorWorkspaceRepos.repo}) = lower(${repo})`
2086
- )
2087
- ).orderBy(asc3(juniorWorkspaces.name));
2088
- return rows.map((row) => row.name);
2089
- }
2090
- async function getWorkspaceByName(db, name) {
2091
- const rows = await db.select().from(juniorWorkspaces).where(eq5(juniorWorkspaces.name, name)).limit(1);
2092
- const workspace = rows[0];
2093
- if (!workspace) return void 0;
2094
- return await workspaceWithSnapshot(
2095
- db,
2096
- workspace,
2097
- await loadWorkspaceRepos(db, workspace.id)
2098
- );
2099
- }
2100
- async function getWorkspace(db, id) {
2101
- const rows = await db.select().from(juniorWorkspaces).where(eq5(juniorWorkspaces.id, id)).limit(1);
2102
- const workspace = rows[0];
2103
- if (!workspace) return void 0;
2104
- return await workspaceWithSnapshot(
2105
- db,
2106
- workspace,
2107
- await loadWorkspaceRepos(db, workspace.id)
2108
- );
2109
- }
2110
- async function createWorkspace(input) {
2111
- const recipe = normalizeWorkspaceRecipe(input);
2112
- const executor = getSqlExecutor();
2113
- const now = /* @__PURE__ */ new Date();
2114
- const id = randomUUID2();
2115
- try {
2116
- await executor.transaction(async () => {
2117
- const db = executor.db();
2118
- await db.insert(juniorWorkspaces).values({
2119
- id,
2120
- name: recipe.name,
2121
- setupScript: recipe.setupScript,
2122
- createdAt: now,
2123
- updatedAt: now
2124
- });
2125
- await replaceWorkspaceRepos(db, id, recipe.repos);
2126
- });
2127
- } catch (error) {
2128
- if (isUniqueViolation(error)) {
2129
- throw new WorkspaceValidationError(
2130
- `Workspace name already exists: ${recipe.name}`
2131
- );
2132
- }
2133
- throw error;
2134
- }
2135
- return await getWorkspace(executor.db(), id);
2136
- }
2137
- async function updateWorkspace(id, input) {
2138
- const recipe = normalizeWorkspaceRecipe(input);
2139
- const executor = getSqlExecutor();
2140
- const now = /* @__PURE__ */ new Date();
2141
- try {
2142
- const result = await executor.transaction(async () => {
2143
- const db = executor.db();
2144
- const existingRows = await db.select().from(juniorWorkspaces).where(eq5(juniorWorkspaces.id, id)).limit(1);
2145
- const existing = existingRows[0];
2146
- if (!existing) return void 0;
2147
- const existingRepos = await loadWorkspaceRepos(db, id);
2148
- const snapshotChanged = existing.setupScript !== recipe.setupScript || !sameWorkspaceRepos(existingRepos, recipe.repos);
2149
- const rows = await db.update(juniorWorkspaces).set({
2150
- name: recipe.name,
2151
- setupScript: recipe.setupScript,
2152
- updatedAt: now
2153
- }).where(eq5(juniorWorkspaces.id, id)).returning();
2154
- await replaceWorkspaceRepos(db, id, recipe.repos);
2155
- const builderNames2 = snapshotChanged ? await clearWorkspaceSnapshots(db, id) : [];
2156
- return { builderNames: builderNames2, workspace: rows[0] };
2157
- });
2158
- if (!result) return void 0;
2159
- await cleanupSnapshotBuilders(id, result.builderNames);
2160
- } catch (error) {
2161
- if (isUniqueViolation(error)) {
2162
- throw new WorkspaceValidationError(
2163
- `Workspace name already exists: ${recipe.name}`
2164
- );
2165
- }
2166
- throw error;
2167
- }
2168
- return await getWorkspace(executor.db(), id);
2169
- }
2170
- async function deleteWorkspace(id) {
2171
- const executor = getSqlExecutor();
2172
- const result = await executor.transaction(async () => {
2173
- const db = executor.db();
2174
- const builderNames2 = await clearWorkspaceSnapshots(db, id);
2175
- const rows = await db.delete(juniorWorkspaces).where(eq5(juniorWorkspaces.id, id)).returning({ id: juniorWorkspaces.id });
2176
- return { builderNames: builderNames2, deleted: rows.length > 0 };
2177
- });
2178
- await cleanupSnapshotBuilders(id, result.builderNames);
2179
- return result.deleted;
2180
- }
2181
-
2182
- // src/chat/code/store.ts
2183
- import { randomUUID as randomUUID3 } from "crypto";
2184
- import { and as and6, eq as eq6, lte, sql as sql3 } from "drizzle-orm";
2185
- import {
2186
- codeChangeInputSchema
2187
- } from "@sentry/junior-plugin-api";
2188
- async function recordCodeChange(db, provider, input) {
2189
- const change = codeChangeInputSchema.parse(input);
2190
- const repositories = await db.insert(juniorCodeRepositories).values({
2191
- id: randomUUID3(),
2192
- name: change.repository.name,
2193
- provider,
2194
- providerId: change.repository.providerId,
2195
- url: change.repository.url,
2196
- updatedAt: change.updatedAt
2197
- }).onConflictDoUpdate({
2198
- target: [
2199
- juniorCodeRepositories.provider,
2200
- juniorCodeRepositories.providerId
2201
- ],
2202
- set: {
2203
- name: change.repository.name,
2204
- ...change.repository.url ? { url: change.repository.url } : void 0,
2205
- updatedAt: change.updatedAt
2206
- },
2207
- where: lte(juniorCodeRepositories.updatedAt, change.updatedAt)
2208
- }).returning({ id: juniorCodeRepositories.id });
2209
- const existingRepositories = repositories[0] ? [] : await db.select({ id: juniorCodeRepositories.id }).from(juniorCodeRepositories).where(
2210
- and6(
2211
- eq6(juniorCodeRepositories.provider, provider),
2212
- eq6(juniorCodeRepositories.providerId, change.repository.providerId)
2213
- )
2214
- ).limit(1);
2215
- const repositoryId = repositories[0]?.id ?? existingRepositories[0]?.id;
2216
- if (!repositoryId) {
2217
- throw new Error("Code repository upsert returned no row");
2218
- }
2219
- const conversationIds = sql3.join(
2220
- change.conversationIds.map((conversationId) => sql3`${conversationId}`),
2221
- sql3`, `
2222
- );
2223
- const values = {
2224
- closedAt: change.closedAt ?? null,
2225
- conversationIds: change.conversationIds.length > 0 ? sql3`ARRAY(
2226
- SELECT DISTINCT value
2227
- FROM unnest(
2228
- ${juniorCodeChanges.conversationIds}
2229
- || ARRAY[${conversationIds}]::text[]
2230
- ) AS value
2231
- ORDER BY value
2232
- )` : juniorCodeChanges.conversationIds,
2233
- mergedAt: change.mergedAt ?? null,
2234
- number: change.number,
2235
- openedAt: change.openedAt,
2236
- repositoryId,
2237
- state: change.state,
2238
- ...change.title ? { title: change.title } : void 0,
2239
- updatedAt: change.updatedAt,
2240
- url: change.url ?? null
2241
- };
2242
- await db.insert(juniorCodeChanges).values({
2243
- ...values,
2244
- conversationIds: change.conversationIds,
2245
- id: randomUUID3(),
2246
- provider,
2247
- providerId: change.providerId
2248
- }).onConflictDoUpdate({
2249
- target: [juniorCodeChanges.provider, juniorCodeChanges.providerId],
2250
- set: values,
2251
- where: lte(juniorCodeChanges.updatedAt, change.updatedAt)
2252
- });
2253
- }
2254
- async function associateCodeChangeConversations(db, provider, input) {
2255
- if (input.conversationIds.length === 0) return;
2256
- const conversationIds = sql3.join(
2257
- input.conversationIds.map((conversationId) => sql3`${conversationId}`),
2258
- sql3`, `
2259
- );
2260
- await db.update(juniorCodeChanges).set({
2261
- conversationIds: sql3`ARRAY(
2262
- SELECT DISTINCT value
2263
- FROM unnest(
2264
- ${juniorCodeChanges.conversationIds}
2265
- || ARRAY[${conversationIds}]::text[]
2266
- ) AS value
2267
- ORDER BY value
2268
- )`
2269
- }).where(
2270
- and6(
2271
- eq6(juniorCodeChanges.provider, provider),
2272
- eq6(juniorCodeChanges.providerId, input.providerId)
2273
- )
2274
- );
2275
- }
2276
-
2277
- // src/chat/code/publisher.ts
2278
- function createCodeChangePublisher(provider) {
2279
- return {
2280
- async associateConversations(input) {
2281
- await associateCodeChangeConversations(getDb(), provider, input);
2282
- },
2283
- async record(input) {
2284
- await recordCodeChange(getDb(), provider, input);
2285
- }
2286
- };
2287
- }
2288
-
2289
- // src/chat/plugins/agent-hooks.ts
2290
- var PluginHookDeniedError = class extends Error {
2291
- constructor(message) {
2292
- super(message);
2293
- this.name = "PluginHookDeniedError";
2294
- }
2295
- };
2296
- var registeredPlugins = [];
2297
- var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
2298
- var PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
2299
- var OPERATIONAL_REPORT_MAX_METRICS = 8;
2300
- var OPERATIONAL_REPORT_MAX_WIDGETS = 12;
2301
- var OPERATIONAL_REPORT_MAX_CHART_SERIES = 8;
2302
- var OPERATIONAL_REPORT_MAX_CHART_CATEGORIES = 100;
2303
- var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
2304
- var OPERATIONAL_REPORT_MAX_FIELDS = 8;
2305
- var OPERATIONAL_REPORT_MAX_RECORDS = 25;
2306
- var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
2307
- var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
2308
- var PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
2309
- "GET",
2310
- "POST",
2311
- "PUT",
2312
- "PATCH",
2313
- "DELETE",
2314
- "HEAD",
2315
- "OPTIONS",
2316
- "ALL"
2317
- ]);
2318
- var PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS = 16e3;
2319
- var PLUGIN_PROMPT_CONTEXT_MAX_BYTES = 8e3;
2320
- var PLUGIN_PROMPT_CONTEXT_TOTAL_MAX_BYTES = 16e3;
2321
- var systemPromptMessageArraySchema = z3.array(promptMessageSchema);
2322
- function isRecord(value) {
2323
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
2324
- }
2325
- function basePluginContext(plugin) {
2326
- const name = plugin.manifest.name;
2327
- return {
2328
- plugin: { name },
2329
- log: createPluginLogger(name),
2330
- db: getDb()
2331
- };
2332
- }
2333
- function pluginToolNamespace(pluginName) {
2334
- const parts = pluginName.split("-").filter(Boolean);
2335
- const [first = "plugin", ...rest] = parts;
2336
- return `${first}${rest.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("")}`;
2337
- }
2338
- function systemPromptPluginContext(plugin) {
2339
- return {
2340
- ...basePluginContext(plugin)
2341
- };
2342
- }
2343
- function pluginInvocationContext(context) {
2344
- const common = {
2345
- conversationId: context.conversationId,
2346
- locationId: context.locationId
2347
- };
2348
- switch (context.source.kind) {
2349
- case "slack": {
2350
- if (context.destination.platform !== "slack") {
2351
- throw new TypeError("Slack plugin context requires Slack destination");
2352
- }
2353
- return {
2354
- ...common,
2355
- actor: context.actor?.platform === "slack" ? context.actor : void 0,
2356
- destination: context.destination,
2357
- source: context.source
2358
- };
2359
- }
2360
- case "local": {
2361
- if (context.destination.platform !== "local") {
2362
- throw new TypeError("Local plugin context requires local destination");
2363
- }
2364
- return {
2365
- ...common,
2366
- actor: context.actor?.platform === "local" ? context.actor : void 0,
2367
- destination: context.destination,
2368
- source: context.source
2369
- };
2370
- }
2371
- case "web":
2372
- return {
2373
- ...common,
2374
- actor: context.actor?.platform === "web" ? context.actor : void 0,
2375
- destination: context.destination,
2376
- source: context.source
2377
- };
2378
- case "resource_event":
2379
- case "scheduled_task":
2380
- case "event_task":
2381
- case "plugin_dispatch":
2382
- case "agent_invocation":
2383
- return {
2384
- ...common,
2385
- actor: context.actor,
2386
- destination: context.destination,
2387
- source: context.source
2388
- };
2389
- }
2390
- }
2391
- function invocationPluginContext(plugin, context, turnId) {
2392
- const base = basePluginContext(plugin);
2393
- const common = {
2394
- ...base,
2395
- conversationId: context.conversationId,
2396
- locationId: context.locationId,
2397
- embedder: createPluginEmbedder(plugin.manifest.name),
2398
- ...context.conversationId && turnId ? {
2399
- events: createPluginConversationEvents({
2400
- conversationId: context.conversationId,
2401
- operationId: `user-prompt:${turnId}`,
2402
- plugin,
2403
- turnId
2404
- })
2405
- } : void 0,
2406
- model: createPluginModel(plugin.manifest.name, plugin.model),
2407
- source: context.source,
2408
- text: context.userText ?? "",
2409
- state: createPluginState(plugin.manifest.name),
2410
- users: {
2411
- resolveActor: context.resolveActorIdentity ?? (async () => void 0)
2412
- }
2413
- };
2414
- return {
2415
- ...common,
2416
- ...pluginInvocationContext(context)
2417
- };
2418
- }
2419
- function pluginMcpContext(plugin, context) {
2420
- const manager = context.mcpToolManager;
2421
- const wrappedTools = new Set(plugin.manifest.mcp?.wrappedTools ?? []);
2422
- if (!manager || wrappedTools.size === 0) {
2423
- return void 0;
2424
- }
2425
- const provider = plugin.manifest.name;
2426
- const prepare = async () => {
2427
- await manager.activateProvider(provider);
2428
- return manager.getActiveProviders().includes(provider) ? "ready" : "authorization_pending";
2429
- };
2430
- return {
2431
- prepare,
2432
- async callTool(input) {
2433
- if (await prepare() === "authorization_pending") {
2434
- return { status: "authorization_pending" };
2435
- }
2436
- return await manager.callWrappedTool(
2437
- provider,
2438
- input.name,
2439
- input.arguments ?? {},
2440
- input.toolCallId ? { toolCallId: input.toolCallId } : void 0
2441
- );
2442
- }
2443
- };
2444
- }
2445
- function safeErrorMessage(error) {
2446
- return error instanceof Error ? error.message : String(error);
2447
- }
2448
- function toPromptContributionContext(args) {
2449
- return {
2450
- id: `${args.hookName}:${args.index}`,
2451
- pluginName: args.pluginName,
2452
- text: args.message.text
2453
- };
2454
- }
2455
- function normalizePromptContext(value) {
2456
- const serialized = JSON.stringify(value);
2457
- if (serialized === void 0 || Buffer.byteLength(serialized, "utf8") > PLUGIN_PROMPT_CONTEXT_MAX_BYTES) {
2458
- throw new TypeError("Plugin prompt context exceeded its serialized budget");
2459
- }
2460
- const parsed = JSON.parse(serialized);
2461
- if (!isRecord(parsed)) {
2462
- throw new TypeError("Plugin prompt context must be a JSON object");
2463
- }
2464
- return parsed;
2465
- }
2466
- function toUserPromptContributionContext(args) {
2467
- const message = promptMessageSchema.safeParse(args.value);
2468
- if (message.success) {
2469
- return toPromptContributionContext({
2470
- hookName: "userPrompt",
2471
- index: args.index,
2472
- message: message.data,
2473
- pluginName: args.pluginName
2474
- });
2475
- }
2476
- if (!isRecord(args.value) || typeof args.value.renderPrompt !== "function") {
2477
- return void 0;
2478
- }
2479
- const context = promptContextSchema.safeParse(args.value.context);
2480
- if (!context.success) {
2481
- return void 0;
2482
- }
2483
- const text = promptMessageSchema.safeParse({
2484
- text: args.value.renderPrompt()
2485
- });
2486
- if (!text.success) {
2487
- return void 0;
2488
- }
2489
- return {
2490
- id: `userPrompt:${args.index}`,
2491
- pluginName: args.pluginName,
2492
- text: text.data.text,
2493
- context: {
2494
- content: normalizePromptContext(context.data.content),
2495
- kind: context.data.kind,
2496
- loadedAtMs: Date.now(),
2497
- pluginName: args.pluginName,
2498
- version: context.data.version
2499
- }
2500
- };
2501
- }
2502
- function logInvalidPromptContributions(args) {
2503
- logWarn("plugin.prompt.contribution_result.invalid", {
2504
- "app.plugin.hook": args.hookName,
2505
- "app.plugin.name": args.pluginName,
2506
- "app.plugin.validation_reason": "invalid_shape"
2507
- });
2508
- }
2509
- function validatePlugins(plugins) {
2510
- const seen = /* @__PURE__ */ new Set();
2511
- for (const plugin of plugins) {
2512
- const name = plugin.manifest.name;
2513
- if (!PLUGIN_NAME_RE.test(name)) {
2514
- throw new Error(
2515
- `Plugin name "${name}" must be a lowercase plugin identifier`
2516
- );
2517
- }
2518
- if (seen.has(name)) {
2519
- throw new Error(`Duplicate plugin name "${name}"`);
2520
- }
2521
- if (plugin.resourceEvents !== void 0 && !pluginResourceEventsSchema.safeParse(plugin.resourceEvents).success) {
2522
- throw new Error(`Plugin "${name}" resourceEvents is invalid`);
2523
- }
2524
- for (const [taskName, task] of Object.entries(plugin.tasks ?? {})) {
2525
- if (!PLUGIN_TOOL_NAME_RE.test(taskName)) {
2526
- throw new Error(
2527
- `Plugin task "${taskName}" from plugin "${name}" must be a camelCase identifier`
2528
- );
2529
- }
2530
- if (typeof task.run !== "function") {
2531
- throw new Error(
2532
- `Plugin task "${taskName}" from plugin "${name}" must define a run function`
2533
- );
2534
- }
2535
- }
2536
- seen.add(name);
2537
- }
2538
- }
2539
- function setPlugins(nextPlugins) {
2540
- validatePlugins(nextPlugins);
2541
- const previous = registeredPlugins;
2542
- registeredPlugins = [...nextPlugins].sort(
2543
- (left, right) => left.manifest.name.localeCompare(right.manifest.name)
2544
- );
2545
- return previous;
2546
- }
2547
- function getPlugins() {
2548
- return [...registeredPlugins];
2549
- }
2550
- function applyPluginFormatMarkdown(text) {
2551
- let transformed = text;
2552
- for (const plugin of getPlugins()) {
2553
- const hook = plugin.hooks?.formatMarkdown;
2554
- if (!hook) {
2555
- continue;
2556
- }
2557
- try {
2558
- const next = hook({ text: transformed });
2559
- if (typeof next === "string") {
2560
- transformed = next;
2561
- }
2562
- } catch (error) {
2563
- logWarn("plugin.format_markdown.hook.failed", {
2564
- "app.plugin.name": plugin.manifest.name,
2565
- "exception.message": safeErrorMessage(error)
2566
- });
2567
- }
2568
- }
2569
- return transformed;
2570
- }
2571
- async function getPluginSystemPromptContributions(platform) {
2572
- const contributions = [];
2573
- let totalChars = 0;
2574
- for (const plugin of getPlugins()) {
2575
- const pluginName = plugin.manifest.name;
2576
- const hook = plugin.hooks?.systemPrompt;
2577
- if (!hook) {
2578
- continue;
2579
- }
2580
- try {
2581
- const pluginContributions = await hook({
2582
- ...systemPromptPluginContext(plugin),
2583
- platform
2584
- });
2585
- const result = systemPromptMessageArraySchema.safeParse(pluginContributions);
2586
- if (!result.success) {
2587
- logInvalidPromptContributions({
2588
- hookName: "systemPrompt",
2589
- pluginName
2590
- });
2591
- continue;
2592
- }
2593
- const acceptedContributions = result.data.map(
2594
- (message, index) => toPromptContributionContext({
2595
- hookName: "systemPrompt",
2596
- index,
2597
- message,
2598
- pluginName
2599
- })
2600
- );
2601
- const pluginContributionChars = acceptedContributions.reduce(
2602
- (sum, contribution) => sum + contribution.text.length,
2603
- 0
2604
- );
2605
- if (totalChars + pluginContributionChars > PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS) {
2606
- logWarn("plugin.system_prompt.contribution_budget_exceeded", {
2607
- "app.plugin.name": pluginName
2608
- });
2609
- continue;
2610
- }
2611
- totalChars += pluginContributionChars;
2612
- contributions.push(...acceptedContributions);
2613
- } catch (error) {
2614
- logWarn("plugin.system_prompt.hook.failed", {
2615
- "app.plugin.name": pluginName,
2616
- "exception.message": safeErrorMessage(error)
2617
- });
2618
- }
2619
- }
2620
- return contributions;
2621
- }
2622
- async function getPluginUserPromptContributions(args) {
2623
- const contributions = [];
2624
- let totalChars = 0;
2625
- let totalContextBytes = 0;
2626
- for (const plugin of getPlugins()) {
2627
- const pluginName = plugin.manifest.name;
2628
- const hook = plugin.hooks?.userPrompt;
2629
- if (!hook) {
2630
- continue;
2631
- }
2632
- try {
2633
- const rawResult = await hook({
2634
- ...invocationPluginContext(plugin, args.context, args.turnId)
2635
- });
2636
- if (rawResult === void 0) {
2637
- continue;
2638
- }
2639
- if (!Array.isArray(rawResult)) {
2640
- logInvalidPromptContributions({
2641
- hookName: "userPrompt",
2642
- pluginName
2643
- });
2644
- continue;
2645
- }
2646
- const acceptedContributions = rawResult.map(
2647
- (value, index) => toUserPromptContributionContext({ index, pluginName, value })
2648
- );
2649
- if (acceptedContributions.some((contribution) => !contribution)) {
2650
- logInvalidPromptContributions({
2651
- hookName: "userPrompt",
2652
- pluginName
2653
- });
2654
- continue;
2655
- }
2656
- const validContributions = acceptedContributions.filter(
2657
- (contribution) => contribution !== void 0
2658
- );
2659
- const pluginContributionChars = validContributions.reduce(
2660
- (sum, contribution) => sum + contribution.text.length,
2661
- 0
2662
- );
2663
- const pluginContextBytes = validContributions.reduce(
2664
- (sum, contribution) => sum + (contribution.context ? Buffer.byteLength(
2665
- JSON.stringify(contribution.context.content),
2666
- "utf8"
2667
- ) : 0),
2668
- 0
2669
- );
2670
- if (totalChars + pluginContributionChars > PLUGIN_PROMPT_CONTRIBUTION_TOTAL_MAX_CHARS || totalContextBytes + pluginContextBytes > PLUGIN_PROMPT_CONTEXT_TOTAL_MAX_BYTES) {
2671
- logWarn("plugin.user_prompt.contribution_budget_exceeded", {
2672
- "app.plugin.name": pluginName
2673
- });
2674
- continue;
2675
- }
2676
- totalChars += pluginContributionChars;
2677
- totalContextBytes += pluginContextBytes;
2678
- contributions.push(...validContributions);
2679
- } catch (error) {
2680
- logWarn("plugin.user_prompt.hook.failed", {
2681
- "app.plugin.name": pluginName,
2682
- "exception.message": safeErrorMessage(error)
2683
- });
2684
- }
2685
- }
2686
- return contributions;
2687
- }
2688
- function getPluginTools(context, sandbox = createSandboxCapability(context.workspace)) {
2689
- const tools = {};
2690
- for (const plugin of getPlugins()) {
2691
- const pluginName = plugin.manifest.name;
2692
- const hook = plugin.hooks?.tools;
2693
- if (!hook) {
2694
- continue;
2695
- }
2696
- const slackToolContext = getSlackToolContext(context);
2697
- const credentialSubject = slackToolContext ? createSlackDirectCredentialSubject({
2698
- channelId: slackToolContext.locationChannelId,
2699
- teamId: slackToolContext.teamId,
2700
- userId: slackToolContext.actor?.userId
2701
- }) : void 0;
2702
- const dashboardConversationUrl = getDashboardConversationLink(
2703
- context.conversationId
2704
- );
2705
- const slackContext = slackToolContext ? {
2706
- channelCapabilities: resolveChannelCapabilities(
2707
- slackToolContext.locationChannelId
2708
- ),
2709
- ...dashboardConversationUrl ? { conversationLink: { url: dashboardConversationUrl } } : void 0,
2710
- ...credentialSubject ? { credentialSubject } : void 0
2711
- } : void 0;
2712
- const annotations = createPluginAnnotations({
2713
- conversationId: context.conversationId,
2714
- db: getDb(),
2715
- plugin: pluginName
2716
- });
2717
- const mcp = pluginMcpContext(plugin, context);
2718
- const canSubscribe = Boolean(plugin.resourceEvents) && plugin.resourceEvents?.isEnabled?.() !== false;
2719
- const resourceEvents = {
2720
- canSubscribe,
2721
- async subscribe(input) {
2722
- if (!canSubscribe) {
2723
- throw new Error(
2724
- "Resource subscriptions are not available in this conversation."
2725
- );
2726
- }
2727
- const registration = plugin.resourceEvents;
2728
- const resourceType = registration?.resourceTypes.find(
2729
- (candidate) => candidate.type === input.resource.type
2730
- );
2731
- if (input.resource.namespace !== pluginName || !resourceType || input.events.length === 0 || input.events.some(
2732
- (eventType) => !resourceType.supportedEvents.includes(eventType)
2733
- )) {
2734
- throw new Error(
2735
- "Resource subscription contains an event or resource that the plugin does not support."
2736
- );
2737
- }
2738
- const subscription = await createResourceEventSubscription({
2739
- conversationId: context.conversationId,
2740
- events: input.events,
2741
- expiresAtMs: Date.now() + RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS,
2742
- intent: input.intent,
2743
- label: input.resource.label,
2744
- namespace: pluginName,
2745
- identifier: normalizeResourceEventIdentifier(
2746
- registration,
2747
- input.resource.identifier
2748
- ),
2749
- resourceType: input.resource.type
2750
- });
2751
- return {
2752
- events: subscription.events,
2753
- id: subscription.id
2754
- };
2755
- }
2756
- };
2757
- const resolveActor = context.resolveActorIdentity ?? (async () => void 0);
2758
- const common = {
2759
- ...basePluginContext(plugin),
2760
- annotations,
2761
- conversationId: context.conversationId,
2762
- locationId: context.locationId,
2763
- ...slackContext ? { slack: slackContext } : void 0,
2764
- userText: context.userText,
2765
- embedder: createPluginEmbedder(pluginName),
2766
- egress: context.egress,
2767
- ...mcp ? { mcp } : void 0,
2768
- model: createPluginModel(pluginName, plugin.model),
2769
- resourceEvents,
2770
- sandbox,
2771
- state: createPluginState(pluginName),
2772
- users: { resolveActor },
2773
- workspaces: {
2774
- async findByRepository(input) {
2775
- return await listWorkspaceNamesByRepository(getDb(), input);
2776
- }
2777
- }
2778
- };
2779
- let pluginContext;
2780
- switch (context.source.kind) {
2781
- case "slack":
2782
- if (context.destination.platform !== "slack") {
2783
- throw new TypeError(
2784
- "Slack plugin context requires Slack destination"
2785
- );
2786
- }
2787
- pluginContext = {
2788
- ...common,
2789
- actor: context.actor?.platform === "slack" ? context.actor : void 0,
2790
- destination: context.destination,
2791
- source: context.source
2792
- };
2793
- break;
2794
- case "local":
2795
- if (context.destination.platform !== "local") {
2796
- throw new TypeError(
2797
- "Local plugin context requires local destination"
2798
- );
2799
- }
2800
- pluginContext = {
2801
- ...common,
2802
- actor: context.actor?.platform === "local" ? context.actor : void 0,
2803
- destination: context.destination,
2804
- source: context.source
2805
- };
2806
- break;
2807
- case "web":
2808
- pluginContext = {
2809
- ...common,
2810
- actor: context.actor?.platform === "web" ? context.actor : void 0,
2811
- destination: context.destination,
2812
- source: context.source
2813
- };
2814
- break;
2815
- case "resource_event":
2816
- case "scheduled_task":
2817
- case "event_task":
2818
- case "plugin_dispatch":
2819
- case "agent_invocation":
2820
- pluginContext = {
2821
- ...common,
2822
- actor: context.actor,
2823
- destination: context.destination,
2824
- source: context.source
2825
- };
2826
- break;
2827
- }
2828
- const pluginTools = hook(pluginContext);
2829
- const namespace = pluginToolNamespace(pluginName);
2830
- for (const [localName, tool] of Object.entries(pluginTools)) {
2831
- if (!PLUGIN_TOOL_NAME_RE.test(localName)) {
2832
- throw new Error(
2833
- `Plugin tool "${localName}" from plugin "${pluginName}" must be a camelCase identifier`
2834
- );
2835
- }
2836
- const name = `${namespace}_${localName}`;
2837
- if (tools[name]) {
2838
- throw new Error(
2839
- `Duplicate plugin tool "${name}" from plugin "${pluginName}"`
2840
- );
2841
- }
2842
- const definition = tool;
2843
- const missingAnnotationKeys = missingToolAnnotationKeys(
2844
- definition.annotations
2845
- );
2846
- if (missingAnnotationKeys.length > 0) {
2847
- logWarn("plugin.tool_annotations.missing", {
2848
- "app.plugin.name": pluginName,
2849
- "gen_ai.tool.name": localName,
2850
- "app.tool.missing_annotations": missingAnnotationKeys.join(",")
2851
- });
2852
- }
2853
- definition.approvalMode ??= "auto";
2854
- definition.identity = {
2855
- id: `${pluginName}.${localName}`,
2856
- name: localName,
2857
- plugin: pluginName
2858
- };
2859
- definition.source = {
2860
- id: pluginName,
2861
- description: plugin.manifest.description
2862
- };
2863
- definition.exposure ??= "deferred";
2864
- tools[name] = definition;
2865
- }
2866
- }
2867
- return tools;
2868
- }
2869
- function routeMethods(route, pluginName) {
2870
- const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
2871
- if (methods.length === 0) {
2872
- throw new Error(
2873
- `Plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
2874
- );
2875
- }
2876
- for (const method of methods) {
2877
- if (!PLUGIN_ROUTE_METHODS.has(method)) {
2878
- throw new Error(
2879
- `Plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
2880
- );
2881
- }
2882
- }
2883
- if (methods.includes("ALL") && methods.length > 1) {
2884
- throw new Error(
2885
- `Plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
2886
- );
2887
- }
2888
- return methods;
2889
- }
2890
- function requirePublishedResourceEvent(plugin, eventType) {
2891
- const registration = plugin.resourceEvents;
2892
- if (!registration || registration.isEnabled?.() === false) {
2893
- throw new Error(
2894
- `Plugin "${plugin.manifest.name}" cannot publish resource events without an active registration`
2895
- );
2896
- }
2897
- if (!registration.resourceTypes.some(
2898
- (resourceType) => resourceType.supportedEvents.includes(eventType)
2899
- )) {
2900
- throw new Error(
2901
- `Plugin "${plugin.manifest.name}" did not register resource event "${eventType}"`
2902
- );
2903
- }
2904
- }
2905
- function getPluginRoutes(options) {
2906
- const routes = [];
2907
- const seen = /* @__PURE__ */ new Set();
2908
- const methodsByPath = /* @__PURE__ */ new Map();
2909
- for (const plugin of getPlugins()) {
2910
- const pluginName = plugin.manifest.name;
2911
- const hook = plugin.hooks?.routes;
2912
- if (!hook) {
2913
- continue;
2914
- }
2915
- const pluginRoutes = hook({
2916
- ...basePluginContext(plugin),
2917
- annotations: {
2918
- forConversation: (conversationId) => createPluginAnnotations({
2919
- conversationId,
2920
- db: getDb(),
2921
- plugin: pluginName
2922
- })
2923
- },
2924
- codeChanges: createCodeChangePublisher(pluginName),
2925
- resourceEvents: {
2926
- async hasMatch(event) {
2927
- if (!options.resourceEvents.hasMatch) return false;
2928
- const parsed = resourceEventInputSchema.parse(event);
2929
- requirePublishedResourceEvent(plugin, parsed.eventType);
2930
- return await options.resourceEvents.hasMatch({
2931
- ...parsed,
2932
- identifier: normalizeResourceEventIdentifier(
2933
- plugin.resourceEvents,
2934
- parsed.identifier
2935
- ),
2936
- namespace: pluginName
2937
- });
2938
- },
2939
- async neededMatchKeys(input) {
2940
- if (!options.resourceEvents.neededMatchKeys) return [];
2941
- const identifiers = [
2942
- ...new Set(
2943
- input.identifiers.map(
2944
- (identifier) => normalizeResourceEventIdentifier(
2945
- plugin.resourceEvents,
2946
- identifier
2947
- )
2948
- ).filter(Boolean)
2949
- )
2950
- ];
2951
- if (identifiers.length === 0 || input.eventTypes.length === 0) {
2952
- return [];
2953
- }
2954
- return await options.resourceEvents.neededMatchKeys({
2955
- eventTypes: input.eventTypes,
2956
- identifiers,
2957
- namespace: pluginName
2958
- });
2959
- },
2960
- async publish(event) {
2961
- const parsed = resourceEventInputSchema.parse(event);
2962
- requirePublishedResourceEvent(plugin, parsed.eventType);
2963
- await options.resourceEvents.publish({
2964
- ...parsed,
2965
- identifier: normalizeResourceEventIdentifier(
2966
- plugin.resourceEvents,
2967
- parsed.identifier
2968
- ),
2969
- namespace: pluginName
2970
- });
2971
- }
2972
- }
2973
- });
2974
- if (!Array.isArray(pluginRoutes)) {
2975
- throw new Error(
2976
- `Plugin routes hook from plugin "${pluginName}" must return an array`
2977
- );
2978
- }
2979
- for (const route of pluginRoutes) {
2980
- if (!isRecord(route)) {
2981
- throw new Error(
2982
- `Plugin route from plugin "${pluginName}" must be an object`
2983
- );
2984
- }
2985
- if (typeof route.path !== "string" || !route.path.startsWith("/")) {
2986
- throw new Error(
2987
- `Plugin route "${route.path}" from plugin "${pluginName}" must start with /`
2988
- );
2989
- }
2990
- if (typeof route.handler !== "function") {
2991
- throw new Error(
2992
- `Plugin route "${route.path}" from plugin "${pluginName}" must provide a handler`
2993
- );
2994
- }
2995
- const methods = routeMethods(route, pluginName);
2996
- const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
2997
- if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
2998
- throw new Error(
2999
- `Plugin route "${route.path}" conflicts with an ALL route for the same path`
3000
- );
3001
- }
3002
- for (const method of methods) {
3003
- const key = `${method}:${route.path}`;
3004
- if (seen.has(key)) {
3005
- throw new Error(`Duplicate plugin route "${method} ${route.path}"`);
3006
- }
3007
- seen.add(key);
3008
- pathMethods.add(method);
3009
- }
3010
- methodsByPath.set(route.path, pathMethods);
3011
- routes.push({
3012
- ...route,
3013
- pluginName
3014
- });
3015
- }
3016
- }
3017
- return routes;
3018
- }
3019
- function getPluginApiRoutes() {
3020
- const routes = [];
3021
- for (const plugin of getPlugins()) {
3022
- const pluginName = plugin.manifest.name;
3023
- const hook = plugin.hooks?.apiRoutes;
3024
- if (!hook) {
3025
- continue;
3026
- }
3027
- const app = hook({
3028
- ...basePluginContext(plugin),
3029
- eventStats: createPluginConversationEventStats(plugin),
3030
- users: { resolve: resolveViewerUser }
3031
- });
3032
- if (app === void 0) {
3033
- continue;
3034
- }
3035
- if (!isRecord(app) || typeof app.fetch !== "function") {
3036
- throw new Error(
3037
- `Plugin apiRoutes hook from plugin "${pluginName}" must return a fetch-compatible app`
3038
- );
3039
- }
3040
- routes.push({ app, pluginName });
3041
- }
3042
- return routes;
3043
- }
3044
- function trustedSlackConversationUrl(pluginName, link) {
3045
- const url = typeof link?.url === "string" ? link.url.trim() : "";
3046
- if (!url) {
3047
- return void 0;
3048
- }
3049
- let parsed;
3050
- try {
3051
- parsed = new URL(url);
3052
- } catch (error) {
3053
- throw new Error(
3054
- `Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
3055
- { cause: error }
3056
- );
3057
- }
3058
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3059
- throw new Error(
3060
- `Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
3061
- );
3062
- }
3063
- return parsed.toString();
3064
- }
3065
- function getPluginSlackConversationLink(conversationId) {
3066
- for (const plugin of getPlugins()) {
3067
- const pluginName = plugin.manifest.name;
3068
- const hook = plugin.hooks?.slackConversationLink;
3069
- if (!hook) {
3070
- continue;
3071
- }
3072
- const link = hook({
3073
- ...basePluginContext(plugin),
3074
- conversationId
3075
- });
3076
- const url = trustedSlackConversationUrl(pluginName, link);
3077
- if (url) {
3078
- return { url };
3079
- }
3080
- }
3081
- return void 0;
3082
- }
3083
- function pluginReadState(state) {
3084
- return {
3085
- get: state.get
3086
- };
3087
- }
3088
- function operationalReportText(value, maxLength) {
3089
- if (typeof value !== "string") {
3090
- return void 0;
3091
- }
3092
- const trimmed = value.trim();
3093
- if (!trimmed) {
3094
- return void 0;
3095
- }
3096
- return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
3097
- }
3098
- function operationalReportTone(tone) {
3099
- return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
3100
- }
3101
- function sanitizeOperationalReport(args) {
3102
- const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
3103
- const label = operationalReportText(
3104
- metric.label,
3105
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3106
- );
3107
- const value = operationalReportText(
3108
- metric.value,
3109
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3110
- );
3111
- if (!label || !value) {
3112
- return void 0;
3113
- }
3114
- const sanitizedMetric = { label, value };
3115
- const tone = operationalReportTone(metric.tone);
3116
- if (tone) {
3117
- sanitizedMetric.tone = tone;
3118
- }
3119
- return sanitizedMetric;
3120
- }).filter((metric) => Boolean(metric));
3121
- const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
3122
- const title2 = operationalReportText(
3123
- recordSet.title,
3124
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3125
- );
3126
- if (!title2) {
3127
- return void 0;
3128
- }
3129
- const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
3130
- const key = operationalReportText(
3131
- field.key,
3132
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3133
- );
3134
- const label = operationalReportText(
3135
- field.label,
3136
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3137
- );
3138
- return key && label ? { key, label } : void 0;
3139
- }).filter((field) => Boolean(field));
3140
- const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
3141
- const id = operationalReportText(
3142
- record.id,
3143
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3144
- ) ?? `${recordSetIndex}:${recordIndex}`;
3145
- const values = Object.fromEntries(
3146
- (fields ?? []).map((field) => [
3147
- field.key,
3148
- operationalReportText(
3149
- record.values[field.key],
3150
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3151
- ) ?? ""
3152
- ])
3153
- );
3154
- const sanitizedRecord = {
3155
- id,
3156
- values
3157
- };
3158
- const tone = operationalReportTone(record.tone);
3159
- if (tone) {
3160
- sanitizedRecord.tone = tone;
3161
- }
3162
- return sanitizedRecord;
3163
- });
3164
- const sanitizedRecordSet = { title: title2 };
3165
- if (fields?.length) {
3166
- sanitizedRecordSet.fields = fields;
3167
- }
3168
- const emptyText = operationalReportText(
3169
- recordSet.emptyText,
3170
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3171
- );
3172
- if (emptyText) {
3173
- sanitizedRecordSet.emptyText = emptyText;
3174
- }
3175
- if (records?.length) {
3176
- sanitizedRecordSet.records = records;
3177
- }
3178
- return sanitizedRecordSet;
3179
- }).filter(
3180
- (recordSet) => Boolean(recordSet)
3181
- );
3182
- const widgets = args.report.widgets?.slice(0, OPERATIONAL_REPORT_MAX_WIDGETS).map((widget, widgetIndex) => {
3183
- const id = operationalReportText(widget.id, OPERATIONAL_REPORT_MAX_LABEL_LENGTH) ?? String(widgetIndex);
3184
- const title2 = operationalReportText(
3185
- widget.title,
3186
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3187
- );
3188
- if (!title2) {
3189
- return void 0;
3190
- }
3191
- const seriesEntries = widget.series.slice(0, OPERATIONAL_REPORT_MAX_CHART_SERIES).map((item) => {
3192
- const key = operationalReportText(
3193
- item.key,
3194
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3195
- );
3196
- const label = operationalReportText(
3197
- item.label,
3198
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3199
- );
3200
- if (!key || !label) {
3201
- return void 0;
3202
- }
3203
- const sanitizedSeries = { key, label };
3204
- if (item.format === "usd") {
3205
- sanitizedSeries.format = "usd";
3206
- }
3207
- const tone = operationalReportTone(item.tone);
3208
- if (tone) {
3209
- sanitizedSeries.tone = tone;
3210
- }
3211
- return { sanitizedSeries, sourceKey: item.key };
3212
- }).filter((item) => Boolean(item));
3213
- if (!seriesEntries.length) {
3214
- return void 0;
3215
- }
3216
- const series = seriesEntries.map((item) => item.sanitizedSeries);
3217
- const categories = widget.categories.slice(-OPERATIONAL_REPORT_MAX_CHART_CATEGORIES).map((category, categoryIndex) => ({
3218
- id: operationalReportText(
3219
- category.id,
3220
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3221
- ) ?? `${widgetIndex}:${categoryIndex}`,
3222
- label: operationalReportText(
3223
- category.label,
3224
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3225
- ) ?? String(categoryIndex + 1),
3226
- values: Object.fromEntries(
3227
- seriesEntries.map(({ sanitizedSeries, sourceKey }) => [
3228
- sanitizedSeries.key,
3229
- Number.isFinite(category.values[sourceKey]) ? category.values[sourceKey] : 0
3230
- ])
3231
- )
3232
- }));
3233
- const sanitizedWidget = {
3234
- categories,
3235
- id,
3236
- series,
3237
- title: title2,
3238
- type: "bar_chart"
3239
- };
3240
- const description = operationalReportText(
3241
- widget.description,
3242
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3243
- );
3244
- if (description) {
3245
- sanitizedWidget.description = description;
3246
- }
3247
- const emptyText = operationalReportText(
3248
- widget.emptyText,
3249
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3250
- );
3251
- if (emptyText) {
3252
- sanitizedWidget.emptyText = emptyText;
3253
- }
3254
- const timeRangeDays = widget.timeRangeDays?.filter(
3255
- (days) => days === 1 || days === 7 || days === 30 || days === 90
3256
- );
3257
- if (timeRangeDays?.length) {
3258
- sanitizedWidget.timeRangeDays = [...new Set(timeRangeDays)];
3259
- }
3260
- return sanitizedWidget;
3261
- }).filter((widget) => Boolean(widget));
3262
- const sanitized = {
3263
- pluginName: args.pluginName
3264
- };
3265
- const generatedAt = operationalReportText(
3266
- args.report.generatedAt,
3267
- OPERATIONAL_REPORT_MAX_VALUE_LENGTH
3268
- );
3269
- if (generatedAt) {
3270
- sanitized.generatedAt = generatedAt;
3271
- }
3272
- if (recordSets?.length) {
3273
- sanitized.recordSets = recordSets;
3274
- }
3275
- if (metrics?.length) {
3276
- sanitized.metrics = metrics;
3277
- }
3278
- if (widgets?.length) {
3279
- sanitized.widgets = widgets;
3280
- }
3281
- const title = operationalReportText(
3282
- args.report.title,
3283
- OPERATIONAL_REPORT_MAX_LABEL_LENGTH
3284
- );
3285
- if (title) {
3286
- sanitized.title = title;
3287
- }
3288
- return sanitized;
3289
- }
3290
- function failedOperationalReport(args) {
3291
- return {
3292
- generatedAt: new Date(args.nowMs).toISOString(),
3293
- pluginName: args.pluginName,
3294
- metrics: [{ label: "report", tone: "danger", value: "failed" }],
3295
- title: args.pluginName,
3296
- recordSets: [
3297
- {
3298
- emptyText: "This plugin report failed to load.",
3299
- title: "Error"
3300
- }
3301
- ]
3302
- };
3303
- }
3304
- async function getPluginOperationalReports(nowMs) {
3305
- const reports = [];
3306
- for (const plugin of getPlugins()) {
3307
- const pluginName = plugin.manifest.name;
3308
- const hook = plugin.hooks?.operationalReport;
3309
- if (!hook) {
3310
- continue;
3311
- }
3312
- try {
3313
- const state = createPluginState(pluginName);
3314
- const report = await hook({
3315
- ...basePluginContext(plugin),
3316
- eventStats: createPluginConversationEventStats(plugin, () => nowMs),
3317
- nowMs,
3318
- state: pluginReadState(state)
3319
- });
3320
- if (!report) {
3321
- continue;
3322
- }
3323
- reports.push(
3324
- sanitizeOperationalReport({
3325
- pluginName,
3326
- report
3327
- })
3328
- );
3329
- } catch (error) {
3330
- const log = createPluginLogger(pluginName);
3331
- log.error("Plugin operational report failed", {
3332
- error: error instanceof Error ? error.message : String(error)
3333
- });
3334
- reports.push(failedOperationalReport({ nowMs, pluginName }));
3335
- }
3336
- }
3337
- return reports;
3338
- }
3339
- async function getPluginProfileReports(args) {
3340
- const reports = [];
3341
- for (const plugin of getPlugins()) {
3342
- const pluginName = plugin.manifest.name;
3343
- const hook = plugin.hooks?.profileReport;
3344
- if (!hook) {
3345
- continue;
3346
- }
3347
- try {
3348
- const state = createPluginState(pluginName);
3349
- const report = await hook({
3350
- ...basePluginContext(plugin),
3351
- nowMs: args.nowMs,
3352
- state: pluginReadState(state),
3353
- subject: args.subject,
3354
- viewer: args.viewer
3355
- });
3356
- if (!report) {
3357
- continue;
3358
- }
3359
- reports.push(
3360
- sanitizeOperationalReport({
3361
- pluginName,
3362
- report
3363
- })
3364
- );
3365
- } catch (error) {
3366
- const log = createPluginLogger(pluginName);
3367
- log.error("Plugin profile report failed", {
3368
- error: error instanceof Error ? error.message : String(error)
3369
- });
3370
- }
3371
- }
3372
- return reports;
3373
- }
3374
- function normalizeEnv(value) {
3375
- if (!isRecord(value)) {
3376
- return {};
3377
- }
3378
- const env = {};
3379
- for (const [key, rawValue] of Object.entries(value)) {
3380
- if (typeof rawValue === "string") {
3381
- env[key] = rawValue;
3382
- }
3383
- }
3384
- return env;
3385
- }
3386
- function preparationSignal(inputSignal, ownerSignal) {
3387
- if (!inputSignal) return ownerSignal;
3388
- if (!ownerSignal) return inputSignal;
3389
- return AbortSignal.any([inputSignal, ownerSignal]);
3390
- }
3391
- function createSandboxCapability(workspace, ownerSignal) {
3392
- return {
3393
- root: SANDBOX_WORKSPACE_ROOT,
3394
- juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
3395
- async readFile(filePath) {
3396
- return await workspace.readFileToBuffer({ path: filePath }) ?? null;
3397
- },
3398
- async run(input) {
3399
- const signal = preparationSignal(input.signal, ownerSignal);
3400
- const result = await runNonInteractiveCommand(workspace, {
3401
- ...input,
3402
- ...signal ? { signal } : void 0
3403
- });
3404
- return {
3405
- exitCode: result.exitCode,
3406
- stdout: result.stdout,
3407
- stderr: result.stderr
3408
- };
3409
- },
3410
- async writeFile(input) {
3411
- await workspace.writeFiles([
3412
- {
3413
- path: input.path,
3414
- content: input.content,
3415
- ...input.mode !== void 0 ? { mode: input.mode } : void 0
3416
- }
3417
- ]);
3418
- }
3419
- };
3420
- }
3421
- function createPluginHookRunner(input = {}) {
3422
- const loaded = getPlugins();
3423
- return {
3424
- async afterMcpTool(tool) {
3425
- for (const plugin of loaded) {
3426
- if (plugin.manifest.name !== tool.provider) {
3427
- continue;
3428
- }
3429
- const hook = plugin.hooks?.afterMcpTool;
3430
- if (!hook) {
3431
- continue;
3432
- }
3433
- const annotations = tool.conversationId ? createPluginAnnotations({
3434
- conversationId: tool.conversationId,
3435
- db: getDb(),
3436
- plugin: plugin.manifest.name
3437
- }) : void 0;
3438
- try {
3439
- await hook({
3440
- ...basePluginContext(plugin),
3441
- ...tool.conversationId ? { conversationId: tool.conversationId } : void 0,
3442
- ...annotations ? { annotations } : void 0,
3443
- result: tool.structuredContent !== void 0 ? { structuredContent: tool.structuredContent } : {},
3444
- tool: {
3445
- arguments: tool.arguments,
3446
- name: tool.toolName
3447
- }
3448
- });
3449
- } catch (error) {
3450
- logWarn("agent.plugin.after_mcp_tool.failed", {
3451
- "app.plugin.name": plugin.manifest.name,
3452
- "gen_ai.tool.name": tool.toolName,
3453
- "exception.message": error instanceof Error ? error.message : String(error)
3454
- });
3455
- }
3456
- }
3457
- },
3458
- async prepareWorkspace(sandbox, repos, signal) {
3459
- const preparers = new Set(
3460
- loaded.filter((plugin) => plugin.hooks?.workspacePrepare).map((plugin) => plugin.manifest.name)
3461
- );
3462
- const unhandledProviders = [
3463
- ...new Set(
3464
- repos.map((repo) => repo.provider).filter((provider) => !preparers.has(provider))
3465
- )
3466
- ].sort();
3467
- if (unhandledProviders.length > 0) {
3468
- throw new Error(
3469
- `Workspace repository providers have no preparation hook: ${unhandledProviders.join(", ")}`
3470
- );
3471
- }
3472
- const selectedRepos = repos.map((repo) => ({
3473
- provider: repo.provider,
3474
- repo: repo.repo,
3475
- path: workspaceRepoCheckoutPath(repo.repo)
3476
- }));
3477
- const paths = /* @__PURE__ */ new Set();
3478
- for (const entry of selectedRepos) {
3479
- const key = entry.path.toLowerCase();
3480
- if (paths.has(key)) {
3481
- throw new Error(`Workspace checkout path collision: ${entry.path}`);
3482
- }
3483
- paths.add(key);
3484
- }
3485
- const sandboxCapability = createSandboxCapability(sandbox, signal);
3486
- for (const plugin of loaded) {
3487
- const hook = plugin.hooks?.workspacePrepare;
3488
- if (!hook) continue;
3489
- const selected = selectedRepos.filter((repo) => repo.provider === plugin.manifest.name).map((repo) => ({
3490
- path: repo.path,
3491
- repo: repo.repo
3492
- }));
3493
- if (selected.length === 0) continue;
3494
- await hook({
3495
- ...basePluginContext(plugin),
3496
- repos: selected,
3497
- sandbox: sandboxCapability
3498
- });
3499
- }
3500
- },
3501
- async prepareSandbox(sandbox) {
3502
- const sandboxCapability = createSandboxCapability(sandbox);
3503
- for (const plugin of loaded) {
3504
- const pluginName = plugin.manifest.name;
3505
- const hook = plugin.hooks?.sandboxPrepare;
3506
- if (!hook) {
3507
- continue;
3508
- }
3509
- logInfo("agent.plugin.sandbox_preparation.started", {
3510
- "app.plugin.name": pluginName
3511
- });
3512
- await hook({
3513
- ...basePluginContext(plugin),
3514
- actor: input.actor,
3515
- sandbox: sandboxCapability
3516
- });
3517
- }
3518
- },
3519
- async beforeToolExecute(tool) {
3520
- const env = {};
3521
- let nextInput = { ...tool.input };
3522
- const actors = input.actors?.() ?? (input.actor ? [input.actor] : []);
3523
- for (const plugin of loaded) {
3524
- const pluginName = plugin.manifest.name;
3525
- const hook = plugin.hooks?.beforeToolExecute;
3526
- if (!hook) {
3527
- continue;
3528
- }
3529
- let replacement;
3530
- let denied;
3531
- await hook({
3532
- ...basePluginContext(plugin),
3533
- actor: input.actor,
3534
- actors,
3535
- tool: {
3536
- name: tool.name,
3537
- input: nextInput
3538
- },
3539
- users: {
3540
- resolveActor: async () => input.actor ? await readActorIdentity(input.actor) : void 0
3541
- },
3542
- env: {
3543
- get(key) {
3544
- return env[key] ?? normalizeEnv(nextInput.env)[key];
3545
- },
3546
- set(key, value) {
3547
- env[key] = value;
3548
- }
3549
- },
3550
- decision: {
3551
- deny(message) {
3552
- denied = message;
3553
- },
3554
- replaceInput(input2) {
3555
- replacement = input2;
3556
- }
3557
- }
3558
- });
3559
- if (denied) {
3560
- throw new PluginHookDeniedError(denied);
3561
- }
3562
- if (replacement !== void 0) {
3563
- if (!isRecord(replacement)) {
3564
- throw new Error(
3565
- `Plugin "${pluginName}" replaced tool input with a non-object value`
3566
- );
3567
- }
3568
- nextInput = { ...replacement };
3569
- }
3570
- }
3571
- return {
3572
- input: nextInput,
3573
- env
3574
- };
3575
- }
3576
- };
3577
- }
3578
-
3579
- export {
3580
- listConversationAnnotations,
3581
- listConversationAnnotationsById,
3582
- createPluginConversationEvents,
3583
- renderPluginConversationEvent,
3584
- createPluginLogger,
3585
- createPluginModel,
3586
- createPluginEmbedder,
3587
- createPluginState,
3588
- setDashboardConversationLinkOptions,
3589
- getDashboardConversationLink,
3590
- getDashboardTaskLink,
3591
- createResourceEventSubscription,
3592
- listResourceEventSubscriptions,
3593
- cancelResourceEventSubscription,
3594
- cancelSubscriptions,
3595
- collectResourceEventMatchKeys,
3596
- findMatchingResourceEventSubscriptions,
3597
- deliverResourceEventSubscription,
3598
- RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS,
3599
- RESOURCE_SUBSCRIPTION_MAX_TTL_MS,
3600
- STOP_WATCHING_TOOL_NAME,
3601
- RESOURCE_WATCH_TOOL_SOURCE,
3602
- getSlackToolContext,
3603
- resolveViewerUser,
3604
- findUserByEmail,
3605
- updateViewerDisplayName,
3606
- readActorIdentity,
3607
- runWithWorkspaceTeamId,
3608
- getWorkspaceTeamId,
3609
- SlackActionError,
3610
- getHeaderString,
3611
- runWithSlackInstallationToken,
3612
- normalizeSlackConversationId,
3613
- withSlackRetries,
3614
- getSlackClient,
3615
- isDmChannel,
3616
- isConversationScopedChannel,
3617
- getFilePermalink,
3618
- downloadPrivateSlackFile,
3619
- bindSlackDirectCredentialSubject,
3620
- verifySlackDirectCredentialSubject,
3621
- bindScheduledTaskCredentialSubject,
3622
- verifyScheduledTaskCredentialSubject,
3623
- bindEventTaskCredentialSubject,
3624
- verifyEventTaskCredentialSubject,
3625
- resolveChannelCapabilities,
3626
- tryWorkspaceRepoCheckoutPath,
3627
- workspaceRepoCheckoutPath,
3628
- getWorkspaceSnapshotBuilder,
3629
- deleteWorkspaceSnapshotBuilders,
3630
- SANDBOX_RUNTIME,
3631
- loadSnapshotsForProfile,
3632
- invalidateReadySnapshot,
3633
- setWorkspaceSnapshot,
3634
- setWorkspaceSnapshotBuild,
3635
- WorkspaceValidationError,
3636
- listWorkspaces,
3637
- getWorkspaceByName,
3638
- getWorkspace,
3639
- createWorkspace,
3640
- updateWorkspace,
3641
- deleteWorkspace,
3642
- PluginHookDeniedError,
3643
- validatePlugins,
3644
- setPlugins,
3645
- getPlugins,
3646
- applyPluginFormatMarkdown,
3647
- getPluginSystemPromptContributions,
3648
- getPluginUserPromptContributions,
3649
- getPluginTools,
3650
- getPluginRoutes,
3651
- getPluginApiRoutes,
3652
- getPluginSlackConversationLink,
3653
- getPluginOperationalReports,
3654
- getPluginProfileReports,
3655
- createPluginHookRunner
3656
- };