@sentry/junior 0.74.0 → 0.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-hooks-2HEB4C3Q.js +33 -0
- package/dist/api-reference.d.ts +1 -1
- package/dist/app.js +5211 -5316
- package/dist/build/copy-build-content.d.ts +1 -1
- package/dist/chat/agent-dispatch/context.d.ts +2 -3
- package/dist/chat/agent-dispatch/types.d.ts +2 -1
- package/dist/chat/config.d.ts +2 -0
- package/dist/chat/conversations/configured.d.ts +2 -0
- package/dist/chat/credentials/subject.d.ts +3 -3
- package/dist/chat/plugins/agent-hooks.d.ts +13 -13
- package/dist/chat/plugins/credential-hooks.d.ts +6 -6
- package/dist/chat/plugins/db.d.ts +31 -0
- package/dist/chat/plugins/logging.d.ts +2 -2
- package/dist/chat/plugins/package-discovery.d.ts +2 -1
- package/dist/chat/plugins/registry.d.ts +4 -0
- package/dist/chat/plugins/state.d.ts +3 -5
- package/dist/chat/plugins/types.d.ts +1 -0
- package/dist/chat/plugins/validation.d.ts +5 -0
- package/dist/chat/prompt.d.ts +11 -1
- package/dist/chat/respond.d.ts +10 -1
- package/dist/chat/runtime/slack-runtime.d.ts +6 -1
- package/dist/chat/sandbox/egress-credentials.d.ts +8 -8
- package/dist/chat/sandbox/sandbox.d.ts +2 -2
- package/dist/chat/sql/db.d.ts +3 -0
- package/dist/chat/sql/executor.d.ts +7 -0
- package/dist/chat/sql/neon.d.ts +2 -4
- package/dist/chat/sql/postgres.d.ts +6 -0
- package/dist/chat/task-execution/state.d.ts +7 -2
- package/dist/chat/task-execution/worker.d.ts +1 -1
- package/dist/chat/tools/agent-tools.d.ts +2 -2
- package/dist/chat/tools/types.d.ts +3 -0
- package/dist/{chunk-7Q5YOUUT.js → chunk-2RWFUS5F.js} +47 -10
- package/dist/{chunk-YRDS7VKO.js → chunk-62FUNJYS.js} +3 -54
- package/dist/{chunk-M4FLLXXD.js → chunk-74HO27II.js} +1 -1
- package/dist/chunk-BNJIEFQC.js +115 -0
- package/dist/{chunk-YOHFWWBV.js → chunk-C3AM4Z4J.js} +1 -103
- package/dist/chunk-D7NFH5GD.js +570 -0
- package/dist/chunk-EE6PJWY4.js +130 -0
- package/dist/{chunk-CYUI7JU5.js → chunk-EIYL7I4S.js} +1 -1
- package/dist/{chunk-GM7HTXYC.js → chunk-FCZO7LAR.js} +13 -2
- package/dist/{chunk-2LUZA3LY.js → chunk-JEELK46E.js} +5 -5
- package/dist/chunk-MCMROINU.js +12 -0
- package/dist/chunk-NPVUAXUE.js +694 -0
- package/dist/{chunk-OR6NQJ5E.js → chunk-OJODNL2P.js} +3 -3
- package/dist/{chunk-3BYAPS6B.js → chunk-OK4KKR7B.js} +1 -11
- package/dist/chunk-OZSPLAQ4.js +71 -0
- package/dist/{chunk-KVZL5NZS.js → chunk-Q3XNY442.js} +17 -7
- package/dist/{chunk-SQGMG7OD.js → chunk-TQ74BATR.js} +100 -58
- package/dist/{chunk-JL2SLRAT.js → chunk-UJ7OTHPO.js} +76 -312
- package/dist/{chunk-HYHKTFG2.js → chunk-VNTLUFTY.js} +80 -843
- package/dist/chunk-WBZ4M5N5.js +59 -0
- package/dist/{chunk-6UP2Z2RZ.js → chunk-XJHDZUGD.js} +7 -7
- package/dist/chunk-Y2CM7HXH.js +111 -0
- package/dist/{chunk-F6HWCPOC.js → chunk-ZNNTSPNF.js} +1 -1
- package/dist/cli/chat.js +52 -2
- package/dist/cli/check.js +6 -5
- package/dist/cli/snapshot-warmup.js +10 -9
- package/dist/cli/upgrade.js +256 -16
- package/dist/db-A3ILH67H.js +20 -0
- package/dist/handlers/sandbox-egress-route.d.ts +4 -0
- package/dist/handlers/slack-webhook.d.ts +4 -0
- package/dist/handlers/webhooks.d.ts +6 -13
- package/dist/nitro.js +34 -89
- package/dist/plugin-module.d.ts +21 -0
- package/dist/plugins-OMJKLRJ2.js +13 -0
- package/dist/plugins.d.ts +6 -4
- package/dist/registry-NLZFIW23.js +46 -0
- package/dist/reporting/conversations.d.ts +3 -3
- package/dist/reporting.d.ts +6 -5
- package/dist/reporting.js +23 -17
- package/dist/{runner-27NP2TEO.js → runner-LUQZ5G67.js} +18 -13
- package/dist/validation-VMCPP3YO.js +15 -0
- package/package.json +11 -9
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPluginLogger,
|
|
3
|
+
createPluginState
|
|
4
|
+
} from "./chunk-BNJIEFQC.js";
|
|
5
|
+
import {
|
|
6
|
+
isConversationChannel,
|
|
7
|
+
isConversationScopedChannel,
|
|
8
|
+
isDmChannel,
|
|
9
|
+
normalizeSlackConversationId
|
|
10
|
+
} from "./chunk-62FUNJYS.js";
|
|
11
|
+
import {
|
|
12
|
+
getPluginDbForRegistration
|
|
13
|
+
} from "./chunk-D7NFH5GD.js";
|
|
14
|
+
import {
|
|
15
|
+
SANDBOX_WORKSPACE_ROOT
|
|
16
|
+
} from "./chunk-G3E7SCME.js";
|
|
17
|
+
import {
|
|
18
|
+
isActorUserId,
|
|
19
|
+
parseActorUserId
|
|
20
|
+
} from "./chunk-EIYL7I4S.js";
|
|
21
|
+
import {
|
|
22
|
+
logInfo
|
|
23
|
+
} from "./chunk-OK4KKR7B.js";
|
|
24
|
+
|
|
25
|
+
// src/chat/tools/slack/context.ts
|
|
26
|
+
function getSlackToolContext(context) {
|
|
27
|
+
if (context.source.platform !== "slack") {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
destination: context.destination?.platform === "slack" ? context.destination : void 0,
|
|
32
|
+
source: context.source,
|
|
33
|
+
requester: context.requester?.platform === "slack" ? context.requester : void 0,
|
|
34
|
+
destinationChannelId: context.destination?.platform === "slack" ? context.destination.channelId : void 0,
|
|
35
|
+
messageTs: context.source.messageTs,
|
|
36
|
+
sourceChannelId: context.source.channelId,
|
|
37
|
+
teamId: context.source.teamId,
|
|
38
|
+
threadTs: context.source.threadTs
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/chat/credentials/subject.ts
|
|
43
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
44
|
+
var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
45
|
+
var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
46
|
+
function getCredentialSubjectSecret() {
|
|
47
|
+
return process.env.JUNIOR_SECRET?.trim() || void 0;
|
|
48
|
+
}
|
|
49
|
+
function buildPayload(input) {
|
|
50
|
+
return [
|
|
51
|
+
CREDENTIAL_SUBJECT_HMAC_CONTEXT,
|
|
52
|
+
input.allowedWhen,
|
|
53
|
+
input.teamId,
|
|
54
|
+
input.channelId,
|
|
55
|
+
input.userId
|
|
56
|
+
].join("\0");
|
|
57
|
+
}
|
|
58
|
+
function signPayload(secret, payload) {
|
|
59
|
+
const digest = createHmac("sha256", secret).update(payload).digest("hex");
|
|
60
|
+
return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
|
|
61
|
+
}
|
|
62
|
+
function timingSafeMatch(expected, actual) {
|
|
63
|
+
const expectedBuffer = Buffer.from(expected);
|
|
64
|
+
const actualBuffer = Buffer.from(actual);
|
|
65
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
69
|
+
}
|
|
70
|
+
function createSlackDirectCredentialSubject(input) {
|
|
71
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
72
|
+
const teamId = input.teamId?.trim();
|
|
73
|
+
const userId = parseActorUserId(input.userId);
|
|
74
|
+
if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
type: "user",
|
|
79
|
+
userId,
|
|
80
|
+
allowedWhen: "private-direct-conversation"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function bindSlackDirectCredentialSubject(input) {
|
|
84
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
85
|
+
const teamId = input.teamId.trim();
|
|
86
|
+
const secret = getCredentialSubjectSecret();
|
|
87
|
+
const { subject } = input;
|
|
88
|
+
const userId = parseActorUserId(subject.userId);
|
|
89
|
+
if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
type: "user",
|
|
94
|
+
userId,
|
|
95
|
+
allowedWhen: subject.allowedWhen,
|
|
96
|
+
binding: {
|
|
97
|
+
type: "slack-direct-conversation",
|
|
98
|
+
teamId,
|
|
99
|
+
channelId,
|
|
100
|
+
signature: signPayload(
|
|
101
|
+
secret,
|
|
102
|
+
buildPayload({
|
|
103
|
+
allowedWhen: subject.allowedWhen,
|
|
104
|
+
teamId,
|
|
105
|
+
channelId,
|
|
106
|
+
userId
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function verifySlackDirectCredentialSubject(input) {
|
|
113
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
114
|
+
const secret = getCredentialSubjectSecret();
|
|
115
|
+
if (!channelId || !secret) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const { subject } = input;
|
|
119
|
+
const binding = subject.binding;
|
|
120
|
+
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) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const expected = signPayload(
|
|
124
|
+
secret,
|
|
125
|
+
buildPayload({
|
|
126
|
+
allowedWhen: subject.allowedWhen,
|
|
127
|
+
teamId: binding.teamId,
|
|
128
|
+
channelId: binding.channelId,
|
|
129
|
+
userId: subject.userId
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
return timingSafeMatch(expected, binding.signature);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/chat/tools/channel-capabilities.ts
|
|
136
|
+
function resolveChannelCapabilities(channelId) {
|
|
137
|
+
return {
|
|
138
|
+
canCreateCanvas: isConversationScopedChannel(channelId),
|
|
139
|
+
canPostToChannel: isConversationChannel(channelId),
|
|
140
|
+
canAddReactions: isConversationScopedChannel(channelId)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/chat/plugins/agent-hooks.ts
|
|
145
|
+
var PluginHookDeniedError = class extends Error {
|
|
146
|
+
constructor(message) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "PluginHookDeniedError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var registeredPlugins = [];
|
|
152
|
+
var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
153
|
+
var PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
154
|
+
var OPERATIONAL_REPORT_MAX_METRICS = 8;
|
|
155
|
+
var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
|
|
156
|
+
var OPERATIONAL_REPORT_MAX_FIELDS = 8;
|
|
157
|
+
var OPERATIONAL_REPORT_MAX_RECORDS = 25;
|
|
158
|
+
var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
|
|
159
|
+
var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
|
|
160
|
+
var PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
|
|
161
|
+
"GET",
|
|
162
|
+
"POST",
|
|
163
|
+
"PUT",
|
|
164
|
+
"PATCH",
|
|
165
|
+
"DELETE",
|
|
166
|
+
"HEAD",
|
|
167
|
+
"OPTIONS",
|
|
168
|
+
"ALL"
|
|
169
|
+
]);
|
|
170
|
+
function isRecord(value) {
|
|
171
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
172
|
+
}
|
|
173
|
+
function basePluginContext(plugin) {
|
|
174
|
+
const name = plugin.manifest.name;
|
|
175
|
+
const db = getPluginDbForRegistration(plugin);
|
|
176
|
+
return {
|
|
177
|
+
plugin: { name },
|
|
178
|
+
log: createPluginLogger(name),
|
|
179
|
+
...db ? { db } : {}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function validatePlugins(plugins) {
|
|
183
|
+
const seen = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const plugin of plugins) {
|
|
185
|
+
const name = plugin.manifest.name;
|
|
186
|
+
if (!PLUGIN_NAME_RE.test(name)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Plugin name "${name}" must be a lowercase plugin identifier`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (seen.has(name)) {
|
|
192
|
+
throw new Error(`Duplicate plugin name "${name}"`);
|
|
193
|
+
}
|
|
194
|
+
seen.add(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function setPlugins(nextPlugins) {
|
|
198
|
+
validatePlugins(nextPlugins);
|
|
199
|
+
const previous = registeredPlugins;
|
|
200
|
+
registeredPlugins = [...nextPlugins].sort(
|
|
201
|
+
(left, right) => left.manifest.name.localeCompare(right.manifest.name)
|
|
202
|
+
);
|
|
203
|
+
return previous;
|
|
204
|
+
}
|
|
205
|
+
function getPlugins() {
|
|
206
|
+
return [...registeredPlugins];
|
|
207
|
+
}
|
|
208
|
+
function getPluginTools(context) {
|
|
209
|
+
const tools = {};
|
|
210
|
+
for (const plugin of getPlugins()) {
|
|
211
|
+
const pluginName = plugin.manifest.name;
|
|
212
|
+
const hook = plugin.hooks?.tools;
|
|
213
|
+
if (!hook) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const destination = context.destination;
|
|
217
|
+
const slackToolContext = getSlackToolContext(context);
|
|
218
|
+
const credentialSubject = slackToolContext ? createSlackDirectCredentialSubject({
|
|
219
|
+
channelId: slackToolContext.sourceChannelId,
|
|
220
|
+
teamId: slackToolContext.teamId,
|
|
221
|
+
userId: slackToolContext.requester?.userId
|
|
222
|
+
}) : void 0;
|
|
223
|
+
const slackContext = slackToolContext ? {
|
|
224
|
+
channelCapabilities: resolveChannelCapabilities(
|
|
225
|
+
slackToolContext.sourceChannelId
|
|
226
|
+
),
|
|
227
|
+
...credentialSubject ? { credentialSubject } : {}
|
|
228
|
+
} : void 0;
|
|
229
|
+
const pluginContext = context.source.platform === "slack" ? {
|
|
230
|
+
...basePluginContext(plugin),
|
|
231
|
+
requester: context.requester?.platform === "slack" ? context.requester : void 0,
|
|
232
|
+
conversationId: context.conversationId,
|
|
233
|
+
destination: destination?.platform === "slack" ? destination : void 0,
|
|
234
|
+
slack: slackContext,
|
|
235
|
+
source: context.source,
|
|
236
|
+
userText: context.userText,
|
|
237
|
+
state: createPluginState(pluginName)
|
|
238
|
+
} : {
|
|
239
|
+
...basePluginContext(plugin),
|
|
240
|
+
requester: context.requester?.platform === "local" ? context.requester : void 0,
|
|
241
|
+
conversationId: context.conversationId,
|
|
242
|
+
destination: destination?.platform === "local" ? destination : void 0,
|
|
243
|
+
source: context.source,
|
|
244
|
+
userText: context.userText,
|
|
245
|
+
state: createPluginState(pluginName)
|
|
246
|
+
};
|
|
247
|
+
const pluginTools = hook(pluginContext);
|
|
248
|
+
for (const [name, tool] of Object.entries(pluginTools)) {
|
|
249
|
+
if (!PLUGIN_TOOL_NAME_RE.test(name)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Plugin tool "${name}" from plugin "${pluginName}" must be a camelCase identifier`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (tools[name]) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Duplicate plugin tool "${name}" from plugin "${pluginName}"`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
tools[name] = tool;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return tools;
|
|
263
|
+
}
|
|
264
|
+
function routeMethods(route, pluginName) {
|
|
265
|
+
const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
|
|
266
|
+
if (methods.length === 0) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
for (const method of methods) {
|
|
272
|
+
if (!PLUGIN_ROUTE_METHODS.has(method)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (methods.includes("ALL") && methods.length > 1) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return methods;
|
|
284
|
+
}
|
|
285
|
+
function getPluginRoutes() {
|
|
286
|
+
const routes = [];
|
|
287
|
+
const seen = /* @__PURE__ */ new Set();
|
|
288
|
+
const methodsByPath = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const plugin of getPlugins()) {
|
|
290
|
+
const pluginName = plugin.manifest.name;
|
|
291
|
+
const hook = plugin.hooks?.routes;
|
|
292
|
+
if (!hook) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const pluginRoutes = hook({
|
|
296
|
+
...basePluginContext(plugin)
|
|
297
|
+
});
|
|
298
|
+
if (!Array.isArray(pluginRoutes)) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Plugin routes hook from plugin "${pluginName}" must return an array`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
for (const route of pluginRoutes) {
|
|
304
|
+
if (!isRecord(route)) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Plugin route from plugin "${pluginName}" must be an object`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (typeof route.path !== "string" || !route.path.startsWith("/")) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Plugin route "${route.path}" from plugin "${pluginName}" must start with /`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (typeof route.handler !== "function") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Plugin route "${route.path}" from plugin "${pluginName}" must provide a handler`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const methods = routeMethods(route, pluginName);
|
|
320
|
+
const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
|
|
321
|
+
if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Plugin route "${route.path}" conflicts with an ALL route for the same path`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
for (const method of methods) {
|
|
327
|
+
const key = `${method}:${route.path}`;
|
|
328
|
+
if (seen.has(key)) {
|
|
329
|
+
throw new Error(`Duplicate plugin route "${method} ${route.path}"`);
|
|
330
|
+
}
|
|
331
|
+
seen.add(key);
|
|
332
|
+
pathMethods.add(method);
|
|
333
|
+
}
|
|
334
|
+
methodsByPath.set(route.path, pathMethods);
|
|
335
|
+
routes.push({
|
|
336
|
+
...route,
|
|
337
|
+
pluginName
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return routes;
|
|
342
|
+
}
|
|
343
|
+
function trustedSlackConversationUrl(pluginName, link) {
|
|
344
|
+
const url = typeof link?.url === "string" ? link.url.trim() : "";
|
|
345
|
+
if (!url) {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
let parsed;
|
|
349
|
+
try {
|
|
350
|
+
parsed = new URL(url);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
|
|
354
|
+
{ cause: error }
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return parsed.toString();
|
|
363
|
+
}
|
|
364
|
+
function getPluginSlackConversationLink(conversationId) {
|
|
365
|
+
for (const plugin of getPlugins()) {
|
|
366
|
+
const pluginName = plugin.manifest.name;
|
|
367
|
+
const hook = plugin.hooks?.slackConversationLink;
|
|
368
|
+
if (!hook) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const link = hook({
|
|
372
|
+
...basePluginContext(plugin),
|
|
373
|
+
conversationId
|
|
374
|
+
});
|
|
375
|
+
const url = trustedSlackConversationUrl(pluginName, link);
|
|
376
|
+
if (url) {
|
|
377
|
+
return { url };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function pluginReadState(state) {
|
|
383
|
+
return {
|
|
384
|
+
get: state.get
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function operationalReportText(value, maxLength) {
|
|
388
|
+
if (typeof value !== "string") {
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
const trimmed = value.trim();
|
|
392
|
+
if (!trimmed) {
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
395
|
+
return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
396
|
+
}
|
|
397
|
+
function operationalReportTone(tone) {
|
|
398
|
+
return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
|
|
399
|
+
}
|
|
400
|
+
function sanitizeOperationalReport(args) {
|
|
401
|
+
const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
|
|
402
|
+
const label = operationalReportText(
|
|
403
|
+
metric.label,
|
|
404
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
405
|
+
);
|
|
406
|
+
const value = operationalReportText(
|
|
407
|
+
metric.value,
|
|
408
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
409
|
+
);
|
|
410
|
+
if (!label || !value) {
|
|
411
|
+
return void 0;
|
|
412
|
+
}
|
|
413
|
+
const sanitizedMetric = { label, value };
|
|
414
|
+
const tone = operationalReportTone(metric.tone);
|
|
415
|
+
if (tone) {
|
|
416
|
+
sanitizedMetric.tone = tone;
|
|
417
|
+
}
|
|
418
|
+
return sanitizedMetric;
|
|
419
|
+
}).filter((metric) => Boolean(metric));
|
|
420
|
+
const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
|
|
421
|
+
const title2 = operationalReportText(
|
|
422
|
+
recordSet.title,
|
|
423
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
424
|
+
);
|
|
425
|
+
if (!title2) {
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
|
|
429
|
+
const key = operationalReportText(
|
|
430
|
+
field.key,
|
|
431
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
432
|
+
);
|
|
433
|
+
const label = operationalReportText(
|
|
434
|
+
field.label,
|
|
435
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
436
|
+
);
|
|
437
|
+
return key && label ? { key, label } : void 0;
|
|
438
|
+
}).filter((field) => Boolean(field));
|
|
439
|
+
const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
|
|
440
|
+
const id = operationalReportText(
|
|
441
|
+
record.id,
|
|
442
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
443
|
+
) ?? `${recordSetIndex}:${recordIndex}`;
|
|
444
|
+
const values = Object.fromEntries(
|
|
445
|
+
(fields ?? []).map((field) => [
|
|
446
|
+
field.key,
|
|
447
|
+
operationalReportText(
|
|
448
|
+
record.values[field.key],
|
|
449
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
450
|
+
) ?? ""
|
|
451
|
+
])
|
|
452
|
+
);
|
|
453
|
+
const sanitizedRecord = {
|
|
454
|
+
id,
|
|
455
|
+
values
|
|
456
|
+
};
|
|
457
|
+
const tone = operationalReportTone(record.tone);
|
|
458
|
+
if (tone) {
|
|
459
|
+
sanitizedRecord.tone = tone;
|
|
460
|
+
}
|
|
461
|
+
return sanitizedRecord;
|
|
462
|
+
});
|
|
463
|
+
const sanitizedRecordSet = { title: title2 };
|
|
464
|
+
if (fields?.length) {
|
|
465
|
+
sanitizedRecordSet.fields = fields;
|
|
466
|
+
}
|
|
467
|
+
const emptyText = operationalReportText(
|
|
468
|
+
recordSet.emptyText,
|
|
469
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
470
|
+
);
|
|
471
|
+
if (emptyText) {
|
|
472
|
+
sanitizedRecordSet.emptyText = emptyText;
|
|
473
|
+
}
|
|
474
|
+
if (records?.length) {
|
|
475
|
+
sanitizedRecordSet.records = records;
|
|
476
|
+
}
|
|
477
|
+
return sanitizedRecordSet;
|
|
478
|
+
}).filter(
|
|
479
|
+
(recordSet) => Boolean(recordSet)
|
|
480
|
+
);
|
|
481
|
+
const sanitized = {
|
|
482
|
+
pluginName: args.pluginName
|
|
483
|
+
};
|
|
484
|
+
const generatedAt = operationalReportText(
|
|
485
|
+
args.report.generatedAt,
|
|
486
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
487
|
+
);
|
|
488
|
+
if (generatedAt) {
|
|
489
|
+
sanitized.generatedAt = generatedAt;
|
|
490
|
+
}
|
|
491
|
+
if (recordSets?.length) {
|
|
492
|
+
sanitized.recordSets = recordSets;
|
|
493
|
+
}
|
|
494
|
+
if (metrics?.length) {
|
|
495
|
+
sanitized.metrics = metrics;
|
|
496
|
+
}
|
|
497
|
+
const title = operationalReportText(
|
|
498
|
+
args.report.title,
|
|
499
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
500
|
+
);
|
|
501
|
+
if (title) {
|
|
502
|
+
sanitized.title = title;
|
|
503
|
+
}
|
|
504
|
+
return sanitized;
|
|
505
|
+
}
|
|
506
|
+
function failedOperationalReport(args) {
|
|
507
|
+
return {
|
|
508
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
509
|
+
pluginName: args.pluginName,
|
|
510
|
+
metrics: [{ label: "report", tone: "danger", value: "failed" }],
|
|
511
|
+
title: args.pluginName,
|
|
512
|
+
recordSets: [
|
|
513
|
+
{
|
|
514
|
+
emptyText: "This plugin report failed to load.",
|
|
515
|
+
title: "Error"
|
|
516
|
+
}
|
|
517
|
+
]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function getPluginOperationalReports(nowMs, conversations) {
|
|
521
|
+
const reports = [];
|
|
522
|
+
for (const plugin of getPlugins()) {
|
|
523
|
+
const pluginName = plugin.manifest.name;
|
|
524
|
+
const hook = plugin.hooks?.operationalReport;
|
|
525
|
+
if (!hook) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const state = createPluginState(pluginName);
|
|
530
|
+
const report = await hook({
|
|
531
|
+
...basePluginContext(plugin),
|
|
532
|
+
conversations,
|
|
533
|
+
nowMs,
|
|
534
|
+
state: pluginReadState(state)
|
|
535
|
+
});
|
|
536
|
+
if (!report) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
reports.push(
|
|
540
|
+
sanitizeOperationalReport({
|
|
541
|
+
pluginName,
|
|
542
|
+
report
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const log = createPluginLogger(pluginName);
|
|
547
|
+
log.error("Plugin operational report failed", {
|
|
548
|
+
error: error instanceof Error ? error.message : String(error)
|
|
549
|
+
});
|
|
550
|
+
reports.push(failedOperationalReport({ nowMs, pluginName }));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return reports;
|
|
554
|
+
}
|
|
555
|
+
function normalizeEnv(value) {
|
|
556
|
+
if (!isRecord(value)) {
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
const env = {};
|
|
560
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
561
|
+
if (typeof rawValue === "string") {
|
|
562
|
+
env[key] = rawValue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return env;
|
|
566
|
+
}
|
|
567
|
+
function createSandboxCapability(sandbox) {
|
|
568
|
+
return {
|
|
569
|
+
root: SANDBOX_WORKSPACE_ROOT,
|
|
570
|
+
juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
|
|
571
|
+
async readFile(filePath) {
|
|
572
|
+
return await sandbox.readFileToBuffer({ path: filePath }) ?? null;
|
|
573
|
+
},
|
|
574
|
+
async run(input) {
|
|
575
|
+
const result = await sandbox.runCommand(input);
|
|
576
|
+
const [stdout, stderr] = await Promise.all([
|
|
577
|
+
result.stdout(),
|
|
578
|
+
result.stderr()
|
|
579
|
+
]);
|
|
580
|
+
return {
|
|
581
|
+
exitCode: result.exitCode,
|
|
582
|
+
stdout,
|
|
583
|
+
stderr
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
async writeFile(input) {
|
|
587
|
+
await sandbox.writeFiles([
|
|
588
|
+
{
|
|
589
|
+
path: input.path,
|
|
590
|
+
content: input.content,
|
|
591
|
+
...input.mode !== void 0 ? { mode: input.mode } : {}
|
|
592
|
+
}
|
|
593
|
+
]);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function createPluginHookRunner(input = {}) {
|
|
598
|
+
const loaded = getPlugins();
|
|
599
|
+
return {
|
|
600
|
+
async prepareSandbox(sandbox) {
|
|
601
|
+
const sandboxCapability = createSandboxCapability(sandbox);
|
|
602
|
+
for (const plugin of loaded) {
|
|
603
|
+
const pluginName = plugin.manifest.name;
|
|
604
|
+
const hook = plugin.hooks?.sandboxPrepare;
|
|
605
|
+
if (!hook) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
logInfo(
|
|
609
|
+
"agent_plugin_hook_sandbox_prepare",
|
|
610
|
+
{},
|
|
611
|
+
{ "app.plugin.name": pluginName },
|
|
612
|
+
"Running agent plugin sandbox prepare hook"
|
|
613
|
+
);
|
|
614
|
+
await hook({
|
|
615
|
+
...basePluginContext(plugin),
|
|
616
|
+
requester: input.requester,
|
|
617
|
+
sandbox: sandboxCapability
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
async beforeToolExecute(tool) {
|
|
622
|
+
let nextInput = { ...tool.input };
|
|
623
|
+
const env = normalizeEnv(nextInput.env);
|
|
624
|
+
for (const plugin of loaded) {
|
|
625
|
+
const pluginName = plugin.manifest.name;
|
|
626
|
+
const hook = plugin.hooks?.beforeToolExecute;
|
|
627
|
+
if (!hook) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
let replacement;
|
|
631
|
+
let denied;
|
|
632
|
+
await hook({
|
|
633
|
+
...basePluginContext(plugin),
|
|
634
|
+
requester: input.requester,
|
|
635
|
+
tool: {
|
|
636
|
+
name: tool.name,
|
|
637
|
+
input: nextInput
|
|
638
|
+
},
|
|
639
|
+
env: {
|
|
640
|
+
get(key) {
|
|
641
|
+
return env[key];
|
|
642
|
+
},
|
|
643
|
+
set(key, value) {
|
|
644
|
+
env[key] = value;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
decision: {
|
|
648
|
+
deny(message) {
|
|
649
|
+
denied = message;
|
|
650
|
+
},
|
|
651
|
+
replaceInput(input2) {
|
|
652
|
+
replacement = input2;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (denied) {
|
|
657
|
+
throw new PluginHookDeniedError(denied);
|
|
658
|
+
}
|
|
659
|
+
if (replacement !== void 0) {
|
|
660
|
+
if (!isRecord(replacement)) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Plugin "${pluginName}" replaced tool input with a non-object value`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
nextInput = { ...replacement };
|
|
666
|
+
Object.assign(env, normalizeEnv(nextInput.env));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
input: {
|
|
671
|
+
...nextInput,
|
|
672
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
673
|
+
},
|
|
674
|
+
env
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export {
|
|
681
|
+
getSlackToolContext,
|
|
682
|
+
bindSlackDirectCredentialSubject,
|
|
683
|
+
verifySlackDirectCredentialSubject,
|
|
684
|
+
resolveChannelCapabilities,
|
|
685
|
+
PluginHookDeniedError,
|
|
686
|
+
validatePlugins,
|
|
687
|
+
setPlugins,
|
|
688
|
+
getPlugins,
|
|
689
|
+
getPluginTools,
|
|
690
|
+
getPluginRoutes,
|
|
691
|
+
getPluginSlackConversationLink,
|
|
692
|
+
getPluginOperationalReports,
|
|
693
|
+
createPluginHookRunner
|
|
694
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPluginForSkillPath,
|
|
3
3
|
getPluginSkillRoots
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2RWFUS5F.js";
|
|
5
5
|
import {
|
|
6
6
|
skillRoots
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Q3XNY442.js";
|
|
8
8
|
import {
|
|
9
9
|
logWarn
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-OK4KKR7B.js";
|
|
11
11
|
|
|
12
12
|
// src/chat/skills.ts
|
|
13
13
|
import fs from "fs/promises";
|