@sentry/junior 0.203.0 → 0.205.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-D4nu8MQV.js +17521 -0
- package/dist/agent-hooks-DXHpTJ4m.js +4042 -0
- package/dist/agent-runner-C04colea.js +678 -0
- package/dist/api/schema.d.ts +2736 -2639
- package/dist/api/schema.js +4 -169
- package/dist/api.d.ts +23 -17
- package/dist/api.js +3881 -5326
- package/dist/app-Cb776lL8.d.ts +247 -0
- package/dist/app.d.ts +4 -520
- package/dist/app.js +9524 -11875
- package/dist/auth-pause-state-C2P3guS1.js +168 -0
- package/dist/catalog-runtime-CXw3asJD.js +7 -0
- package/dist/checkpoint-CrViJ8Ps.js +2450 -0
- package/dist/cli/chat.d.ts +20 -0
- package/dist/cli/chat.js +229 -336
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +513 -806
- package/dist/cli/env.d.ts +13 -0
- package/dist/cli/env.js +63 -12
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +90 -159
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +43 -64
- package/dist/cli/plugins.d.ts +13 -0
- package/dist/cli/plugins.js +148 -227
- package/dist/cli/run.d.ts +16 -0
- package/dist/cli/run.js +52 -8
- package/dist/cli/snapshot-create.d.ts +4 -0
- package/dist/cli/snapshot-create.js +71 -125
- package/dist/cli/upgrade.d.ts +15 -0
- package/dist/cli/upgrade.js +232 -335
- package/dist/config-BOtJQ7Pc.js +941 -0
- package/dist/context-B45_WqVX.js +298 -0
- package/dist/conversation-privacy-BZRA6Pes.js +182 -0
- package/dist/conversation-work-C6y3T5Hx.js +51 -0
- package/dist/credential-sync-Bep34IB_.js +2661 -0
- package/dist/db-BeyXKJOI.d.ts +5 -0
- package/dist/destinations-BeBiJeEM.js +29 -0
- package/dist/execution-stats-B6lgWxCK.js +373 -0
- package/dist/executor-BlUhFL7_.js +721 -0
- package/dist/experimental-CIXf7Uye.js +46 -0
- package/dist/history-ClE4toxe.d.ts +32 -0
- package/dist/history-CqTuSAdI.js +441 -0
- package/dist/instrumentation.d.ts +3 -3
- package/dist/instrumentation.js +130 -176
- package/dist/job-queue-DLMqIK74.js +63 -0
- package/dist/logging-BMJzdDlP.js +1210 -0
- package/dist/model-profile-CZmVoGdX.js +49 -0
- package/dist/nitro.d.ts +31 -36
- package/dist/nitro.js +275 -456
- package/dist/oauth-callback-server-WwG5_Zgm.js +130 -0
- package/dist/oauth-relay-CdCnu80W.js +62 -0
- package/dist/package-discovery-7a9YqLhE.js +455 -0
- package/dist/pending-auth-CUbXq06G.js +185 -0
- package/dist/plugin-module-BKGOXca2.js +81 -0
- package/dist/plugins-BI9A1vKA.d.ts +95 -0
- package/dist/plugins-egKWjgue.js +99 -0
- package/dist/private-trace-result-BLvBjy_e.js +16 -0
- package/dist/profile-T0tXE4zu.js +861 -0
- package/dist/registry-BA0__8ai.js +1419 -0
- package/dist/reporting-schema-DosTN__Q.js +105 -0
- package/dist/resolve-bvD1ORdv.js +546 -0
- package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
- package/dist/runner-BXYxfIUL.js +392 -0
- package/dist/sandbox-egress-signals-ngEPjSEW.js +46 -0
- package/dist/schema-Rn8-XEaN.js +1140 -0
- package/dist/sentry-CD3SdJl-.js +26 -0
- package/dist/skills-DJOlYdBs.js +316 -0
- package/dist/spawn-h0R1bTEo.js +29 -0
- package/dist/stats-3YZn-Swi.js +2648 -0
- package/dist/store-C5qxF2ac.d.ts +208 -0
- package/dist/task-queue-CoXwd2eq.js +136 -0
- package/dist/task-runner-B2_mJZj0.js +639 -0
- package/dist/thread-state-CqZ1m-es.js +431 -0
- package/dist/turn-execution-fI6PZ0iI.js +47 -0
- package/dist/turn-lifecycle-CcnwrOcp.js +54 -0
- package/dist/usage-B5SfMb3H.js +296 -0
- package/dist/usage-schema-CKaDkH_j.js +20 -0
- package/dist/validation-DfWcvhuS.js +40 -0
- package/dist/vercel-queue-DIkVcfK4.js +167 -0
- package/dist/vercel.d.ts +6 -6
- package/dist/vercel.js +14 -6
- package/dist/version-CnXZ614E.d.ts +5 -0
- package/dist/version-oD-oPrbO.js +8 -0
- package/dist/version.d.ts +2 -4
- package/dist/version.js +2 -6
- package/dist/web-authorization-Crbzlmzo.js +76 -0
- package/dist/web-input-BOuq3oKK.js +494 -0
- package/dist/work-B91LQKHr.js +509 -0
- package/dist/worker-BcRRTe6k.js +625 -0
- package/package.json +9 -9
- package/dist/agent-hooks-GB2VPTNA.js +0 -51
- package/dist/catalog-runtime-UBLIWD3O.js +0 -14
- package/dist/chunk-2LYHWMR4.js +0 -46
- package/dist/chunk-352OWAZH.js +0 -320
- package/dist/chunk-3Y47VMHV.js +0 -6
- package/dist/chunk-3YJSKOWF.js +0 -164
- package/dist/chunk-4KSSKTQ5.js +0 -229
- package/dist/chunk-4UO6FK4G.js +0 -64
- package/dist/chunk-5AHX53NQ.js +0 -717
- package/dist/chunk-5J3E5FWA.js +0 -700
- package/dist/chunk-5PDAG6O3.js +0 -206
- package/dist/chunk-6Y5B5PEX.js +0 -80
- package/dist/chunk-7GWQ5S5R.js +0 -2086
- package/dist/chunk-7H2VZTWN.js +0 -103
- package/dist/chunk-7NL7T6VU.js +0 -406
- package/dist/chunk-A5L27LE4.js +0 -387
- package/dist/chunk-ADIAWL34.js +0 -81
- package/dist/chunk-ASTEMBKJ.js +0 -519
- package/dist/chunk-CEMMLV5L.js +0 -98
- package/dist/chunk-CM5HNV2W.js +0 -131
- package/dist/chunk-CRTRBBRI.js +0 -105
- package/dist/chunk-DA2KGZZI.js +0 -13
- package/dist/chunk-DIB4GX3G.js +0 -8
- package/dist/chunk-DKIJ2RP5.js +0 -68
- package/dist/chunk-DWPN6SCH.js +0 -172
- package/dist/chunk-EDKUFGPL.js +0 -64
- package/dist/chunk-ENY6SKIU.js +0 -1228
- package/dist/chunk-F4MGCZOX.js +0 -2943
- package/dist/chunk-FA6POGLH.js +0 -70
- package/dist/chunk-FSUR7GTG.js +0 -20475
- package/dist/chunk-GKEOHJL5.js +0 -1856
- package/dist/chunk-GMCB6W5Y.js +0 -333
- package/dist/chunk-IE2VPG7O.js +0 -1135
- package/dist/chunk-IZ5B5IHQ.js +0 -3656
- package/dist/chunk-JAL3CH4G.js +0 -27
- package/dist/chunk-KIIEUXD3.js +0 -1764
- package/dist/chunk-KXU4NIL7.js +0 -692
- package/dist/chunk-MTM5IFCY.js +0 -1190
- package/dist/chunk-N7TMSOZH.js +0 -235
- package/dist/chunk-NCEBK6PH.js +0 -36
- package/dist/chunk-NHJU2ZAL.js +0 -56
- package/dist/chunk-P6KTRNVC.js +0 -321
- package/dist/chunk-PDO5BLNM.js +0 -22
- package/dist/chunk-PDRNLLFT.js +0 -3295
- package/dist/chunk-PEQXYSSE.js +0 -45
- package/dist/chunk-PKEX2AQX.js +0 -198
- package/dist/chunk-PSJCGIGO.js +0 -1163
- package/dist/chunk-PZUSUL55.js +0 -506
- package/dist/chunk-R45M4TUS.js +0 -37
- package/dist/chunk-RC745GGZ.js +0 -122
- package/dist/chunk-SP3QKZXJ.js +0 -820
- package/dist/chunk-TXKUKMVO.js +0 -417
- package/dist/chunk-UE4GPWSD.js +0 -931
- package/dist/chunk-UIE3R5XU.js +0 -17
- package/dist/chunk-UXPG6ZIN.js +0 -84
- package/dist/chunk-VFUK3X5B.js +0 -50
- package/dist/chunk-VNMJWREK.js +0 -787
- package/dist/chunk-VZ7CSOX2.js +0 -706
- package/dist/chunk-WCNQJKFT.js +0 -10
- package/dist/chunk-WDZR3QQM.js +0 -135
- package/dist/chunk-WLYVJCHV.js +0 -1433
- package/dist/chunk-WS7NAFTM.js +0 -1520
- package/dist/chunk-X6GDDWQH.js +0 -60
- package/dist/chunk-XPDEYNQI.js +0 -102
- package/dist/chunk-XRW44VGM.js +0 -58
- package/dist/chunk-Y2CM7HXH.js +0 -111
- package/dist/chunk-Y7X25LFY.js +0 -15
- package/dist/conversation-work-VIOQ5ZF3.js +0 -54
- package/dist/db-NGDJV7YH.js +0 -26
- package/dist/discovery-DWVDM7UK.js +0 -44
- package/dist/experimental-P5FZM4CU.js +0 -12
- package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
- package/dist/oauth-relay-5XPVRPYB.js +0 -8
- package/dist/plugins-YTYJMMJF.js +0 -14
- package/dist/runner-ORTNSB2Q.js +0 -522
- package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
- package/dist/sentry-MB3NL3IR.js +0 -42
- package/dist/skills-H376WQOH.js +0 -27
- package/dist/spawn-T43WHLOS.js +0 -42
- package/dist/task-runner-LBZWF32V.js +0 -36
- package/dist/validation-REJLRKI6.js +0 -17
- package/dist/work-3KJ3RE76.js +0 -51
- package/dist/worker-QFEBU72E.js +0 -24
package/dist/chunk-IZ5B5IHQ.js
DELETED
|
@@ -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-GKEOHJL5.js";
|
|
9
|
-
import {
|
|
10
|
-
juniorCodeChanges,
|
|
11
|
-
juniorCodeRepositories,
|
|
12
|
-
juniorConversationAnnotations,
|
|
13
|
-
juniorConversationEvents,
|
|
14
|
-
juniorIdentities,
|
|
15
|
-
juniorSnapshots,
|
|
16
|
-
juniorUsers,
|
|
17
|
-
juniorWorkspaceRepos,
|
|
18
|
-
juniorWorkspaces
|
|
19
|
-
} from "./chunk-IE2VPG7O.js";
|
|
20
|
-
import {
|
|
21
|
-
SANDBOX_WORKSPACE_ROOT,
|
|
22
|
-
getVercelSandboxCredentials,
|
|
23
|
-
hash,
|
|
24
|
-
isSandboxMissingError,
|
|
25
|
-
runNonInteractiveCommand,
|
|
26
|
-
sleep
|
|
27
|
-
} from "./chunk-5AHX53NQ.js";
|
|
28
|
-
import {
|
|
29
|
-
getStateAdapter
|
|
30
|
-
} from "./chunk-P6KTRNVC.js";
|
|
31
|
-
import {
|
|
32
|
-
botConfig,
|
|
33
|
-
completeObject,
|
|
34
|
-
embedTexts,
|
|
35
|
-
getSlackBotToken
|
|
36
|
-
} from "./chunk-ENY6SKIU.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-WS7NAFTM.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
|
-
};
|