@sentry/junior 0.8.0 → 0.9.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/chunk-4G2LA7RO.js +678 -0
- package/dist/{chunk-OGLG4WAL.js → chunk-DIMXJUSL.js} +11346 -9421
- package/dist/{chunk-NNYZHUWR.js → chunk-I3DYWLM6.js} +15 -6
- package/dist/chunk-IJVZEV3K.js +840 -0
- package/dist/{chunk-Z5E25LRN.js → chunk-KCLEEKYX.js} +124 -17
- package/dist/chunk-VM3CPAZF.js +448 -0
- package/dist/chunk-ZBWWHP6Q.js +1436 -0
- package/dist/{chunk-PY4AI2GZ.js → chunk-ZW4OVKF5.js} +376 -79
- package/dist/cli/check.js +4 -2
- package/dist/cli/snapshot-warmup.js +7 -6
- package/dist/handlers/queue-callback.js +7 -7
- package/dist/handlers/router.d.ts +1 -0
- package/dist/handlers/router.js +523 -85
- package/dist/handlers/webhooks.js +2 -2
- package/dist/next-config.js +1 -1
- package/dist/production-XMCJXOOI.js +15 -0
- package/package.json +12 -14
- package/dist/bot-7SE3TX37.js +0 -19
- package/dist/chunk-H3ZG43WE.js +0 -330
- package/dist/chunk-KT5HARSN.js +0 -164
- package/dist/chunk-RKOO42TW.js +0 -1797
- package/dist/chunk-VW26MOSO.js +0 -522
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPluginRuntimeDependencies,
|
|
3
|
+
getPluginRuntimePostinstall
|
|
4
|
+
} from "./chunk-ZBWWHP6Q.js";
|
|
5
|
+
import {
|
|
6
|
+
withSpan
|
|
7
|
+
} from "./chunk-ZW4OVKF5.js";
|
|
8
|
+
|
|
9
|
+
// src/chat/config.ts
|
|
10
|
+
var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
|
|
11
|
+
var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
|
|
12
|
+
var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
|
|
13
|
+
var TURN_TIMEOUT_BUFFER_SECONDS = 20;
|
|
14
|
+
function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
|
|
15
|
+
const value = Number.parseInt(rawValue ?? "", 10);
|
|
16
|
+
if (Number.isNaN(value)) {
|
|
17
|
+
return Math.max(
|
|
18
|
+
MIN_AGENT_TURN_TIMEOUT_MS,
|
|
19
|
+
Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
|
|
23
|
+
}
|
|
24
|
+
function resolveQueueCallbackMaxDurationSeconds(env) {
|
|
25
|
+
const value = Number.parseInt(
|
|
26
|
+
env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "",
|
|
27
|
+
10
|
|
28
|
+
);
|
|
29
|
+
if (Number.isNaN(value) || value <= 0) {
|
|
30
|
+
return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
|
|
35
|
+
const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
|
|
36
|
+
return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
|
|
37
|
+
}
|
|
38
|
+
function toOptionalTrimmed(value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
44
|
+
}
|
|
45
|
+
function readBotConfig(env) {
|
|
46
|
+
const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds(env);
|
|
47
|
+
const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(
|
|
48
|
+
queueCallbackMaxDurationSeconds
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
userName: env.JUNIOR_BOT_NAME ?? "junior",
|
|
52
|
+
modelId: env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
|
|
53
|
+
fastModelId: env.AI_FAST_MODEL ?? env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
|
|
54
|
+
turnTimeoutMs: parseAgentTurnTimeoutMs(
|
|
55
|
+
env.AGENT_TURN_TIMEOUT_MS,
|
|
56
|
+
maxTurnTimeoutMs
|
|
57
|
+
)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function readChatConfig(env = process.env) {
|
|
61
|
+
return {
|
|
62
|
+
bot: readBotConfig(env),
|
|
63
|
+
queue: {
|
|
64
|
+
callbackMaxDurationSeconds: resolveQueueCallbackMaxDurationSeconds(env)
|
|
65
|
+
},
|
|
66
|
+
slack: {
|
|
67
|
+
botToken: toOptionalTrimmed(env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(env.SLACK_BOT_USER_TOKEN),
|
|
68
|
+
signingSecret: toOptionalTrimmed(env.SLACK_SIGNING_SECRET),
|
|
69
|
+
clientId: toOptionalTrimmed(env.SLACK_CLIENT_ID),
|
|
70
|
+
clientSecret: toOptionalTrimmed(env.SLACK_CLIENT_SECRET)
|
|
71
|
+
},
|
|
72
|
+
state: {
|
|
73
|
+
adapter: env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory" ? "memory" : "redis",
|
|
74
|
+
redisUrl: toOptionalTrimmed(env.REDIS_URL)
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
var chatConfig = readChatConfig(process.env);
|
|
79
|
+
function getChatConfig() {
|
|
80
|
+
return chatConfig;
|
|
81
|
+
}
|
|
82
|
+
var botConfig = chatConfig.bot;
|
|
83
|
+
function getSlackBotToken() {
|
|
84
|
+
return chatConfig.slack.botToken;
|
|
85
|
+
}
|
|
86
|
+
function getSlackSigningSecret() {
|
|
87
|
+
return chatConfig.slack.signingSecret;
|
|
88
|
+
}
|
|
89
|
+
function getSlackClientId() {
|
|
90
|
+
return chatConfig.slack.clientId;
|
|
91
|
+
}
|
|
92
|
+
function getSlackClientSecret() {
|
|
93
|
+
return chatConfig.slack.clientSecret;
|
|
94
|
+
}
|
|
95
|
+
function getRuntimeMetadata() {
|
|
96
|
+
return {
|
|
97
|
+
version: toOptionalTrimmed(process.env.VERCEL_GIT_COMMIT_SHA)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/chat/state/adapter.ts
|
|
102
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
103
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
104
|
+
var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
|
|
105
|
+
var stateAdapter;
|
|
106
|
+
var redisStateAdapter;
|
|
107
|
+
function createQueuedStateAdapter(base) {
|
|
108
|
+
const acquireLock = async (threadId, ttlMs) => {
|
|
109
|
+
const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
|
|
110
|
+
return await base.acquireLock(threadId, effectiveTtlMs);
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
appendToList: (key, value, options) => base.appendToList(key, value, options),
|
|
114
|
+
connect: () => base.connect(),
|
|
115
|
+
disconnect: () => base.disconnect(),
|
|
116
|
+
subscribe: (threadId) => base.subscribe(threadId),
|
|
117
|
+
unsubscribe: (threadId) => base.unsubscribe(threadId),
|
|
118
|
+
isSubscribed: (threadId) => base.isSubscribed(threadId),
|
|
119
|
+
acquireLock,
|
|
120
|
+
releaseLock: (lock) => base.releaseLock(lock),
|
|
121
|
+
extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
|
|
122
|
+
forceReleaseLock: (threadId) => base.forceReleaseLock(threadId),
|
|
123
|
+
get: (key) => base.get(key),
|
|
124
|
+
getList: (key) => base.getList(key),
|
|
125
|
+
set: (key, value, ttlMs) => base.set(key, value, ttlMs),
|
|
126
|
+
setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
|
|
127
|
+
delete: (key) => base.delete(key)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function createStateAdapter() {
|
|
131
|
+
const config = getChatConfig();
|
|
132
|
+
if (config.state.adapter === "memory") {
|
|
133
|
+
redisStateAdapter = void 0;
|
|
134
|
+
return createQueuedStateAdapter(createMemoryState());
|
|
135
|
+
}
|
|
136
|
+
if (!config.state.redisUrl) {
|
|
137
|
+
throw new Error("REDIS_URL is required for durable Slack thread state");
|
|
138
|
+
}
|
|
139
|
+
const redisState = createRedisState({
|
|
140
|
+
url: config.state.redisUrl
|
|
141
|
+
});
|
|
142
|
+
redisStateAdapter = redisState;
|
|
143
|
+
return createQueuedStateAdapter(redisState);
|
|
144
|
+
}
|
|
145
|
+
function getOptionalRedisStateAdapter() {
|
|
146
|
+
getStateAdapter();
|
|
147
|
+
return redisStateAdapter;
|
|
148
|
+
}
|
|
149
|
+
async function getConnectedStateContext() {
|
|
150
|
+
const adapter = getStateAdapter();
|
|
151
|
+
await adapter.connect();
|
|
152
|
+
return {
|
|
153
|
+
redisStateAdapter: getOptionalRedisStateAdapter(),
|
|
154
|
+
stateAdapter: adapter
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function getStateAdapter() {
|
|
158
|
+
if (!stateAdapter) {
|
|
159
|
+
stateAdapter = createStateAdapter();
|
|
160
|
+
}
|
|
161
|
+
return stateAdapter;
|
|
162
|
+
}
|
|
163
|
+
async function disconnectStateAdapter() {
|
|
164
|
+
if (!stateAdapter) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await stateAdapter.disconnect();
|
|
169
|
+
} finally {
|
|
170
|
+
stateAdapter = void 0;
|
|
171
|
+
redisStateAdapter = void 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/chat/sandbox/runtime-dependency-snapshots.ts
|
|
176
|
+
import { createHash } from "crypto";
|
|
177
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
178
|
+
|
|
179
|
+
// src/chat/sandbox/noninteractive-command.ts
|
|
180
|
+
var NON_INTERACTIVE_ENV = {
|
|
181
|
+
CI: "1",
|
|
182
|
+
TERM: "dumb",
|
|
183
|
+
NO_COLOR: "1",
|
|
184
|
+
PAGER: "cat",
|
|
185
|
+
GIT_PAGER: "cat",
|
|
186
|
+
GH_PROMPT_DISABLED: "1",
|
|
187
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
188
|
+
GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
|
|
189
|
+
GH_SPINNER_DISABLED: "1",
|
|
190
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
191
|
+
GCM_INTERACTIVE: "never",
|
|
192
|
+
DEBIAN_FRONTEND: "noninteractive"
|
|
193
|
+
};
|
|
194
|
+
function shellQuote(value) {
|
|
195
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
196
|
+
}
|
|
197
|
+
function buildEnvExports(options) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
if (options.pathPrefix) {
|
|
200
|
+
lines.push(`export PATH="${options.pathPrefix}"`);
|
|
201
|
+
}
|
|
202
|
+
for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) {
|
|
203
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
204
|
+
}
|
|
205
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
206
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
207
|
+
}
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
|
210
|
+
function toCommandScript(input) {
|
|
211
|
+
return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(
|
|
212
|
+
" "
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
function buildNonInteractiveShellScript(script, options = {}) {
|
|
216
|
+
return [...buildEnvExports(options), "exec </dev/null", script].join(" && ");
|
|
217
|
+
}
|
|
218
|
+
function buildNonInteractiveCommand(input) {
|
|
219
|
+
return {
|
|
220
|
+
cmd: "bash",
|
|
221
|
+
args: [
|
|
222
|
+
input.login ? "-lc" : "-c",
|
|
223
|
+
buildNonInteractiveShellScript(toCommandScript(input), {
|
|
224
|
+
env: input.env,
|
|
225
|
+
pathPrefix: input.pathPrefix
|
|
226
|
+
})
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function runNonInteractiveCommand(runner, input) {
|
|
231
|
+
return await runner.runCommand({
|
|
232
|
+
...buildNonInteractiveCommand(input),
|
|
233
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
234
|
+
...input.sudo !== void 0 ? { sudo: input.sudo } : {}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/chat/sandbox/credentials.ts
|
|
239
|
+
function toOptionalTrimmed2(value) {
|
|
240
|
+
if (!value) {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
const trimmed = value.trim();
|
|
244
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
245
|
+
}
|
|
246
|
+
function getVercelSandboxCredentials() {
|
|
247
|
+
const token = toOptionalTrimmed2(process.env.VERCEL_TOKEN);
|
|
248
|
+
const teamId = toOptionalTrimmed2(process.env.VERCEL_TEAM_ID);
|
|
249
|
+
const projectId = toOptionalTrimmed2(process.env.VERCEL_PROJECT_ID);
|
|
250
|
+
if (!token && !teamId && !projectId) {
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
if (!token || !teamId || !projectId) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"Missing Vercel Sandbox credentials: set VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID together."
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
token,
|
|
260
|
+
teamId,
|
|
261
|
+
projectId
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/chat/sandbox/paths.ts
|
|
266
|
+
function normalizeWorkspaceRoot(input) {
|
|
267
|
+
const candidate = (input ?? "").trim();
|
|
268
|
+
if (!candidate) {
|
|
269
|
+
return "/vercel/sandbox";
|
|
270
|
+
}
|
|
271
|
+
const normalized = candidate.replace(/\/+$/, "");
|
|
272
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
273
|
+
}
|
|
274
|
+
var SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(process.env.VERCEL_SANDBOX_WORKSPACE_DIR);
|
|
275
|
+
var SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
|
|
276
|
+
function sandboxSkillDir(skillName) {
|
|
277
|
+
return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/chat/sandbox/runtime-dependency-snapshots.ts
|
|
281
|
+
var SNAPSHOT_CACHE_PREFIX = "junior:sandbox_snapshot_profile";
|
|
282
|
+
var SNAPSHOT_LOCK_PREFIX = "junior:sandbox_snapshot_lock";
|
|
283
|
+
var SNAPSHOT_PROFILE_VERSION = 1;
|
|
284
|
+
var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
285
|
+
var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
|
|
286
|
+
var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
|
|
287
|
+
var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
288
|
+
function sleep(ms) {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
setTimeout(resolve, ms);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function profileCacheKey(profileHash) {
|
|
294
|
+
return `${SNAPSHOT_CACHE_PREFIX}:${profileHash}`;
|
|
295
|
+
}
|
|
296
|
+
function profileLockKey(profileHash) {
|
|
297
|
+
return `${SNAPSHOT_LOCK_PREFIX}:${profileHash}`;
|
|
298
|
+
}
|
|
299
|
+
function isExactNpmVersion(version) {
|
|
300
|
+
return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
|
|
301
|
+
}
|
|
302
|
+
function hasFloatingSelector(dep) {
|
|
303
|
+
return dep.type === "npm" && !isExactNpmVersion(dep.version);
|
|
304
|
+
}
|
|
305
|
+
function parseFloatingDepMaxAgeMs() {
|
|
306
|
+
const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
|
|
307
|
+
if (!raw?.trim()) {
|
|
308
|
+
return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
|
|
309
|
+
}
|
|
310
|
+
const parsed = Number.parseInt(raw, 10);
|
|
311
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
312
|
+
return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
|
|
313
|
+
}
|
|
314
|
+
return parsed;
|
|
315
|
+
}
|
|
316
|
+
function buildDependencyProfile(runtime) {
|
|
317
|
+
const dependencies = getPluginRuntimeDependencies();
|
|
318
|
+
const postinstall = getPluginRuntimePostinstall();
|
|
319
|
+
if (dependencies.length === 0 && postinstall.length === 0) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
|
|
323
|
+
const hasFloatingVersions = dependencies.some((dep) => hasFloatingSelector(dep)) || postinstall.length > 0;
|
|
324
|
+
const hashInput = JSON.stringify({
|
|
325
|
+
version: SNAPSHOT_PROFILE_VERSION,
|
|
326
|
+
runtime,
|
|
327
|
+
rebuildEpoch,
|
|
328
|
+
dependencies,
|
|
329
|
+
postinstall
|
|
330
|
+
});
|
|
331
|
+
const profileHash = createHash("sha256").update(hashInput).digest("hex");
|
|
332
|
+
return {
|
|
333
|
+
profileHash,
|
|
334
|
+
dependencyCount: dependencies.length,
|
|
335
|
+
hasFloatingVersions,
|
|
336
|
+
dependencies,
|
|
337
|
+
postinstall
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function getRuntimeDependencyProfileHash(runtime) {
|
|
341
|
+
return buildDependencyProfile(runtime)?.profileHash;
|
|
342
|
+
}
|
|
343
|
+
function shouldRebuildCachedSnapshot(profile, cached) {
|
|
344
|
+
if (!profile.hasFloatingVersions) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const maxAgeMs = parseFloatingDepMaxAgeMs();
|
|
348
|
+
if (maxAgeMs === 0) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
return Date.now() - cached.createdAtMs > maxAgeMs;
|
|
352
|
+
}
|
|
353
|
+
async function getCachedSnapshot(profileHash) {
|
|
354
|
+
try {
|
|
355
|
+
const state = getStateAdapter();
|
|
356
|
+
await state.connect();
|
|
357
|
+
const raw = await state.get(profileCacheKey(profileHash));
|
|
358
|
+
if (typeof raw !== "string") {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const parsed = JSON.parse(raw);
|
|
362
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.profileHash !== "string" || typeof parsed.snapshotId !== "string" || typeof parsed.runtime !== "string" || typeof parsed.createdAtMs !== "number" || typeof parsed.dependencyCount !== "number") {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return parsed;
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function setCachedSnapshot(entry) {
|
|
371
|
+
const state = getStateAdapter();
|
|
372
|
+
await state.connect();
|
|
373
|
+
await state.set(
|
|
374
|
+
profileCacheKey(entry.profileHash),
|
|
375
|
+
JSON.stringify(entry),
|
|
376
|
+
SNAPSHOT_CACHE_TTL_MS
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
async function withSnapshotSpan(name, op, attributes, callback) {
|
|
380
|
+
return await withSpan(name, op, {}, callback, attributes);
|
|
381
|
+
}
|
|
382
|
+
async function runOrThrow(sandbox, params, label) {
|
|
383
|
+
const result = await runNonInteractiveCommand(sandbox, params);
|
|
384
|
+
if (result.exitCode === 0) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const stderr = (await result.stderr()).trim();
|
|
388
|
+
const stdout = (await result.stdout()).trim();
|
|
389
|
+
const detail = stderr || stdout || "command failed";
|
|
390
|
+
throw new Error(`${label} failed: ${detail}`);
|
|
391
|
+
}
|
|
392
|
+
async function tryRun(sandbox, params) {
|
|
393
|
+
const result = await runNonInteractiveCommand(sandbox, params);
|
|
394
|
+
if (result.exitCode === 0) {
|
|
395
|
+
return { ok: true };
|
|
396
|
+
}
|
|
397
|
+
const stderr = (await result.stderr()).trim();
|
|
398
|
+
const stdout = (await result.stdout()).trim();
|
|
399
|
+
return { ok: false, detail: stderr || stdout || "command failed" };
|
|
400
|
+
}
|
|
401
|
+
async function installGhCliViaDnf(sandbox) {
|
|
402
|
+
const direct = await tryRun(sandbox, {
|
|
403
|
+
cmd: "dnf",
|
|
404
|
+
args: ["install", "-y", "gh"],
|
|
405
|
+
sudo: true
|
|
406
|
+
});
|
|
407
|
+
if (direct.ok) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const dnf5Repo = await tryRun(sandbox, {
|
|
411
|
+
cmd: "dnf",
|
|
412
|
+
args: [
|
|
413
|
+
"config-manager",
|
|
414
|
+
"addrepo",
|
|
415
|
+
"--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
|
|
416
|
+
],
|
|
417
|
+
sudo: true
|
|
418
|
+
});
|
|
419
|
+
if (!dnf5Repo.ok) {
|
|
420
|
+
await runOrThrow(
|
|
421
|
+
sandbox,
|
|
422
|
+
{
|
|
423
|
+
cmd: "dnf",
|
|
424
|
+
args: ["install", "-y", "dnf-command(config-manager)"],
|
|
425
|
+
sudo: true
|
|
426
|
+
},
|
|
427
|
+
"dnf install dnf-command(config-manager)"
|
|
428
|
+
);
|
|
429
|
+
await runOrThrow(
|
|
430
|
+
sandbox,
|
|
431
|
+
{
|
|
432
|
+
cmd: "dnf",
|
|
433
|
+
args: [
|
|
434
|
+
"config-manager",
|
|
435
|
+
"--add-repo",
|
|
436
|
+
"https://cli.github.com/packages/rpm/gh-cli.repo"
|
|
437
|
+
],
|
|
438
|
+
sudo: true
|
|
439
|
+
},
|
|
440
|
+
"dnf config-manager --add-repo gh-cli.repo"
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
await runOrThrow(
|
|
444
|
+
sandbox,
|
|
445
|
+
{
|
|
446
|
+
cmd: "dnf",
|
|
447
|
+
args: ["install", "-y", "gh", "--repo", "gh-cli"],
|
|
448
|
+
sudo: true
|
|
449
|
+
},
|
|
450
|
+
"dnf install gh --repo gh-cli"
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
function runtimeDependencyFilePath(url, sha256) {
|
|
454
|
+
let urlBasename = "package.rpm";
|
|
455
|
+
try {
|
|
456
|
+
const pathname = new URL(url).pathname;
|
|
457
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
458
|
+
const candidate = segments[segments.length - 1];
|
|
459
|
+
if (candidate) {
|
|
460
|
+
urlBasename = candidate;
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
}
|
|
464
|
+
const sanitizedBasename = urlBasename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
465
|
+
return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
|
|
466
|
+
}
|
|
467
|
+
async function installRuntimeDependencies(sandbox, deps) {
|
|
468
|
+
const systemDeps = deps.filter(
|
|
469
|
+
(dep) => dep.type === "system"
|
|
470
|
+
);
|
|
471
|
+
const npmPackages = deps.filter(
|
|
472
|
+
(dep) => dep.type === "npm"
|
|
473
|
+
).map((dep) => `${dep.package}@${dep.version}`);
|
|
474
|
+
if (systemDeps.length > 0) {
|
|
475
|
+
await withSnapshotSpan(
|
|
476
|
+
"sandbox.snapshot.install_system",
|
|
477
|
+
"sandbox.snapshot.install.system",
|
|
478
|
+
{
|
|
479
|
+
"app.sandbox.snapshot.install.system_count": systemDeps.length
|
|
480
|
+
},
|
|
481
|
+
async () => {
|
|
482
|
+
for (const dep of systemDeps) {
|
|
483
|
+
if ("url" in dep) {
|
|
484
|
+
const rpmPath = runtimeDependencyFilePath(dep.url, dep.sha256);
|
|
485
|
+
await runOrThrow(
|
|
486
|
+
sandbox,
|
|
487
|
+
{
|
|
488
|
+
cmd: "curl",
|
|
489
|
+
args: ["-fsSL", dep.url, "-o", rpmPath]
|
|
490
|
+
},
|
|
491
|
+
`curl download ${dep.url}`
|
|
492
|
+
);
|
|
493
|
+
const checksumResult = await runNonInteractiveCommand(sandbox, {
|
|
494
|
+
cmd: "sha256sum",
|
|
495
|
+
args: [rpmPath]
|
|
496
|
+
});
|
|
497
|
+
const checksumStdout = (await checksumResult.stdout()).trim();
|
|
498
|
+
const checksumStderr = (await checksumResult.stderr()).trim();
|
|
499
|
+
if (checksumResult.exitCode !== 0) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
|
|
505
|
+
if (!actualChecksum) {
|
|
506
|
+
throw new Error("sha256sum produced empty output");
|
|
507
|
+
}
|
|
508
|
+
if (actualChecksum !== dep.sha256) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`checksum mismatch for ${dep.url}: expected ${dep.sha256}, got ${actualChecksum}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
await runOrThrow(
|
|
514
|
+
sandbox,
|
|
515
|
+
{
|
|
516
|
+
cmd: "dnf",
|
|
517
|
+
args: ["install", "-y", rpmPath],
|
|
518
|
+
sudo: true
|
|
519
|
+
},
|
|
520
|
+
`dnf install ${dep.url}`
|
|
521
|
+
);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (dep.package === "gh") {
|
|
525
|
+
await installGhCliViaDnf(sandbox);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
await runOrThrow(
|
|
529
|
+
sandbox,
|
|
530
|
+
{
|
|
531
|
+
cmd: "dnf",
|
|
532
|
+
args: ["install", "-y", dep.package],
|
|
533
|
+
sudo: true
|
|
534
|
+
},
|
|
535
|
+
`dnf install ${dep.package}`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (npmPackages.length > 0) {
|
|
542
|
+
await withSnapshotSpan(
|
|
543
|
+
"sandbox.snapshot.install_npm",
|
|
544
|
+
"sandbox.snapshot.install.npm",
|
|
545
|
+
{
|
|
546
|
+
"app.sandbox.snapshot.install.npm_count": npmPackages.length
|
|
547
|
+
},
|
|
548
|
+
async () => {
|
|
549
|
+
await runOrThrow(
|
|
550
|
+
sandbox,
|
|
551
|
+
{
|
|
552
|
+
cmd: "npm",
|
|
553
|
+
args: [
|
|
554
|
+
"install",
|
|
555
|
+
"--global",
|
|
556
|
+
"--prefix",
|
|
557
|
+
`${SANDBOX_WORKSPACE_ROOT}/.junior`,
|
|
558
|
+
...npmPackages
|
|
559
|
+
]
|
|
560
|
+
},
|
|
561
|
+
"npm install"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function runRuntimePostinstall(sandbox, commands) {
|
|
568
|
+
if (commands.length === 0) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
await withSnapshotSpan(
|
|
572
|
+
"sandbox.snapshot.runtime_postinstall",
|
|
573
|
+
"sandbox.snapshot.runtime_postinstall",
|
|
574
|
+
{
|
|
575
|
+
"app.sandbox.snapshot.runtime_postinstall.count": commands.length
|
|
576
|
+
},
|
|
577
|
+
async () => {
|
|
578
|
+
for (const command of commands) {
|
|
579
|
+
const result = await runNonInteractiveCommand(sandbox, {
|
|
580
|
+
cmd: command.cmd,
|
|
581
|
+
args: command.args,
|
|
582
|
+
login: true,
|
|
583
|
+
pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
|
|
584
|
+
...command.sudo !== void 0 ? { sudo: command.sudo } : {}
|
|
585
|
+
});
|
|
586
|
+
if (result.exitCode === 0) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const stderr = (await result.stderr()).trim();
|
|
590
|
+
const stdout = (await result.stdout()).trim();
|
|
591
|
+
const detail = stderr || stdout || "command failed";
|
|
592
|
+
throw new Error(`runtime-postinstall ${command.cmd} failed: ${detail}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
async function createDependencySnapshot(profile, runtime, timeoutMs) {
|
|
598
|
+
return await withSnapshotSpan(
|
|
599
|
+
"sandbox.snapshot.build",
|
|
600
|
+
"sandbox.snapshot.build",
|
|
601
|
+
{
|
|
602
|
+
"app.sandbox.runtime": runtime,
|
|
603
|
+
"app.sandbox.snapshot.dependency_count": profile.dependencyCount
|
|
604
|
+
},
|
|
605
|
+
async () => {
|
|
606
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
607
|
+
const sandbox = await Sandbox.create({
|
|
608
|
+
timeout: timeoutMs,
|
|
609
|
+
runtime,
|
|
610
|
+
...sandboxCredentials ?? {}
|
|
611
|
+
});
|
|
612
|
+
try {
|
|
613
|
+
await installRuntimeDependencies(sandbox, profile.dependencies);
|
|
614
|
+
await runRuntimePostinstall(sandbox, profile.postinstall);
|
|
615
|
+
return await withSnapshotSpan(
|
|
616
|
+
"sandbox.snapshot.capture",
|
|
617
|
+
"sandbox.snapshot.capture",
|
|
618
|
+
{
|
|
619
|
+
"app.sandbox.snapshot.dependency_count": profile.dependencyCount
|
|
620
|
+
},
|
|
621
|
+
async () => {
|
|
622
|
+
const snapshot = await sandbox.snapshot();
|
|
623
|
+
return snapshot.snapshotId;
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
} finally {
|
|
627
|
+
try {
|
|
628
|
+
await sandbox.stop({ blocking: true });
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks) {
|
|
636
|
+
const state = getStateAdapter();
|
|
637
|
+
await state.connect();
|
|
638
|
+
const lockKey = profileLockKey(profileHash);
|
|
639
|
+
const tryAcquireLock = async () => await state.acquireLock(lockKey, SNAPSHOT_BUILD_LOCK_TTL_MS);
|
|
640
|
+
let lock = await tryAcquireLock();
|
|
641
|
+
if (lock) {
|
|
642
|
+
try {
|
|
643
|
+
const result = await callback();
|
|
644
|
+
return {
|
|
645
|
+
snapshotId: result.snapshotId,
|
|
646
|
+
source: result.source,
|
|
647
|
+
waitedForLock: false
|
|
648
|
+
};
|
|
649
|
+
} finally {
|
|
650
|
+
await state.releaseLock(lock);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return await withSnapshotSpan(
|
|
654
|
+
"sandbox.snapshot.lock_wait",
|
|
655
|
+
"sandbox.snapshot.lock_wait",
|
|
656
|
+
{
|
|
657
|
+
"app.sandbox.snapshot.profile_hash": profileHash
|
|
658
|
+
},
|
|
659
|
+
async () => {
|
|
660
|
+
await hooks?.onWaitingForLock?.();
|
|
661
|
+
const waitUntil = Date.now() + SNAPSHOT_WAIT_FOR_LOCK_MS;
|
|
662
|
+
while (Date.now() < waitUntil) {
|
|
663
|
+
const cached2 = await getCachedSnapshot(profileHash);
|
|
664
|
+
if (cached2?.snapshotId && canUseCachedSnapshot(cached2)) {
|
|
665
|
+
return {
|
|
666
|
+
snapshotId: cached2.snapshotId,
|
|
667
|
+
source: "wait_cache",
|
|
668
|
+
waitedForLock: true
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
lock = await tryAcquireLock();
|
|
672
|
+
if (lock) {
|
|
673
|
+
try {
|
|
674
|
+
const result = await callback();
|
|
675
|
+
return {
|
|
676
|
+
snapshotId: result.snapshotId,
|
|
677
|
+
source: result.source,
|
|
678
|
+
waitedForLock: true
|
|
679
|
+
};
|
|
680
|
+
} finally {
|
|
681
|
+
await state.releaseLock(lock);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
await sleep(500);
|
|
685
|
+
}
|
|
686
|
+
const cached = await getCachedSnapshot(profileHash);
|
|
687
|
+
if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
|
|
688
|
+
return {
|
|
689
|
+
snapshotId: cached.snapshotId,
|
|
690
|
+
source: "wait_cache",
|
|
691
|
+
waitedForLock: true
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
throw new Error("Timed out waiting for snapshot build lock");
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
function toResolveOutcome(forceRebuild, source, waitedForLock) {
|
|
699
|
+
if (source === "built") {
|
|
700
|
+
return forceRebuild ? "forced_rebuild" : "rebuilt";
|
|
701
|
+
}
|
|
702
|
+
if (waitedForLock || source === "wait_cache") {
|
|
703
|
+
return "cache_hit_after_lock_wait";
|
|
704
|
+
}
|
|
705
|
+
return "cache_hit";
|
|
706
|
+
}
|
|
707
|
+
function getRebuildReason(params) {
|
|
708
|
+
if (params.forceRebuild) {
|
|
709
|
+
return params.staleSnapshotId ? "snapshot_missing" : "force_rebuild";
|
|
710
|
+
}
|
|
711
|
+
if (params.cached?.snapshotId && params.shouldRebuildCached) {
|
|
712
|
+
return "floating_stale";
|
|
713
|
+
}
|
|
714
|
+
if (!params.cached?.snapshotId) {
|
|
715
|
+
return "cache_miss";
|
|
716
|
+
}
|
|
717
|
+
return void 0;
|
|
718
|
+
}
|
|
719
|
+
async function resolveRuntimeDependencySnapshot(params) {
|
|
720
|
+
return await withSnapshotSpan(
|
|
721
|
+
"sandbox.snapshot.resolve",
|
|
722
|
+
"sandbox.snapshot.resolve",
|
|
723
|
+
{
|
|
724
|
+
"app.sandbox.runtime": params.runtime,
|
|
725
|
+
"app.sandbox.snapshot.force_rebuild": Boolean(params.forceRebuild)
|
|
726
|
+
},
|
|
727
|
+
async () => {
|
|
728
|
+
await params.onProgress?.("resolve_start");
|
|
729
|
+
const resolveStartedAtMs = Date.now();
|
|
730
|
+
const profile = buildDependencyProfile(params.runtime);
|
|
731
|
+
if (!profile) {
|
|
732
|
+
return {
|
|
733
|
+
dependencyCount: 0,
|
|
734
|
+
cacheHit: false,
|
|
735
|
+
resolveOutcome: "no_profile"
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const cached = await getCachedSnapshot(profile.profileHash);
|
|
739
|
+
const cachedNeedsRebuild = Boolean(
|
|
740
|
+
cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
|
|
741
|
+
);
|
|
742
|
+
if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
|
|
743
|
+
await params.onProgress?.("cache_hit");
|
|
744
|
+
return {
|
|
745
|
+
snapshotId: cached.snapshotId,
|
|
746
|
+
profileHash: profile.profileHash,
|
|
747
|
+
dependencyCount: profile.dependencyCount,
|
|
748
|
+
cacheHit: true,
|
|
749
|
+
resolveOutcome: "cache_hit"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const rebuildReason = getRebuildReason({
|
|
753
|
+
forceRebuild: params.forceRebuild,
|
|
754
|
+
staleSnapshotId: params.staleSnapshotId,
|
|
755
|
+
cached,
|
|
756
|
+
shouldRebuildCached: cachedNeedsRebuild
|
|
757
|
+
});
|
|
758
|
+
const canUseCachedSnapshot = (candidate) => {
|
|
759
|
+
if (params.forceRebuild) {
|
|
760
|
+
if (params.staleSnapshotId) {
|
|
761
|
+
return candidate.snapshotId !== params.staleSnapshotId;
|
|
762
|
+
}
|
|
763
|
+
return candidate.createdAtMs > resolveStartedAtMs;
|
|
764
|
+
}
|
|
765
|
+
return !shouldRebuildCachedSnapshot(profile, candidate);
|
|
766
|
+
};
|
|
767
|
+
const lockResult = await withBuildLock(
|
|
768
|
+
profile.profileHash,
|
|
769
|
+
async () => {
|
|
770
|
+
const latest = await getCachedSnapshot(profile.profileHash);
|
|
771
|
+
if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
|
|
772
|
+
await params.onProgress?.("cache_hit");
|
|
773
|
+
return {
|
|
774
|
+
snapshotId: latest.snapshotId,
|
|
775
|
+
source: "callback_cache"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
await params.onProgress?.("building_snapshot");
|
|
779
|
+
const nextSnapshotId = await createDependencySnapshot(
|
|
780
|
+
profile,
|
|
781
|
+
params.runtime,
|
|
782
|
+
params.timeoutMs
|
|
783
|
+
);
|
|
784
|
+
await setCachedSnapshot({
|
|
785
|
+
profileHash: profile.profileHash,
|
|
786
|
+
snapshotId: nextSnapshotId,
|
|
787
|
+
runtime: params.runtime,
|
|
788
|
+
createdAtMs: Date.now(),
|
|
789
|
+
dependencyCount: profile.dependencyCount
|
|
790
|
+
});
|
|
791
|
+
await params.onProgress?.("build_complete");
|
|
792
|
+
return { snapshotId: nextSnapshotId, source: "built" };
|
|
793
|
+
},
|
|
794
|
+
canUseCachedSnapshot,
|
|
795
|
+
{
|
|
796
|
+
onWaitingForLock: async () => {
|
|
797
|
+
await params.onProgress?.("waiting_for_lock");
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
);
|
|
801
|
+
return {
|
|
802
|
+
snapshotId: lockResult.snapshotId,
|
|
803
|
+
profileHash: profile.profileHash,
|
|
804
|
+
dependencyCount: profile.dependencyCount,
|
|
805
|
+
cacheHit: lockResult.source !== "built",
|
|
806
|
+
resolveOutcome: toResolveOutcome(
|
|
807
|
+
Boolean(params.forceRebuild),
|
|
808
|
+
lockResult.source,
|
|
809
|
+
lockResult.waitedForLock
|
|
810
|
+
),
|
|
811
|
+
...rebuildReason ? { rebuildReason } : {}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
function isSnapshotMissingError(error) {
|
|
817
|
+
const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
818
|
+
return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export {
|
|
822
|
+
botConfig,
|
|
823
|
+
getSlackBotToken,
|
|
824
|
+
getSlackSigningSecret,
|
|
825
|
+
getSlackClientId,
|
|
826
|
+
getSlackClientSecret,
|
|
827
|
+
getRuntimeMetadata,
|
|
828
|
+
getConnectedStateContext,
|
|
829
|
+
getStateAdapter,
|
|
830
|
+
disconnectStateAdapter,
|
|
831
|
+
SANDBOX_WORKSPACE_ROOT,
|
|
832
|
+
SANDBOX_SKILLS_ROOT,
|
|
833
|
+
sandboxSkillDir,
|
|
834
|
+
buildNonInteractiveShellScript,
|
|
835
|
+
runNonInteractiveCommand,
|
|
836
|
+
getVercelSandboxCredentials,
|
|
837
|
+
getRuntimeDependencyProfileHash,
|
|
838
|
+
resolveRuntimeDependencySnapshot,
|
|
839
|
+
isSnapshotMissingError
|
|
840
|
+
};
|