@sentry/junior 0.57.0 → 0.59.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/app.js +1311 -1284
- package/dist/chat/agent-dispatch/store.d.ts +4 -2
- package/dist/chat/agent-dispatch/types.d.ts +0 -1
- package/dist/chat/conversation-privacy.d.ts +23 -0
- package/dist/chat/logging.d.ts +2 -0
- package/dist/chat/mcp/tool-manager.d.ts +18 -5
- package/dist/chat/mcp/tool-name.d.ts +2 -0
- package/dist/chat/pi/client.d.ts +2 -0
- package/dist/chat/pi/derived-state.d.ts +5 -0
- package/dist/chat/pi/traced-stream.d.ts +5 -1
- package/dist/chat/prompt.d.ts +3 -9
- package/dist/chat/respond-helpers.d.ts +5 -3
- package/dist/chat/respond.d.ts +1 -0
- package/dist/chat/runtime/conversation-message.d.ts +10 -0
- package/dist/chat/runtime/processing-reaction.d.ts +2 -4
- package/dist/chat/runtime/reply-executor.d.ts +13 -16
- package/dist/chat/runtime/slack-runtime.d.ts +19 -32
- package/dist/chat/runtime/thread-state.d.ts +1 -1
- package/dist/chat/runtime/turn-input.d.ts +29 -0
- package/dist/chat/runtime/turn-preparation.d.ts +4 -24
- package/dist/chat/runtime/turn.d.ts +2 -3
- package/dist/chat/sentry-links.d.ts +4 -0
- package/dist/chat/services/context-compaction.d.ts +3 -4
- package/dist/chat/services/pending-auth.d.ts +1 -1
- package/dist/chat/services/subscribed-reply-policy.d.ts +2 -13
- package/dist/chat/services/timeout-resume.d.ts +1 -2
- package/dist/chat/services/turn-session-record.d.ts +82 -0
- package/dist/chat/slack/assistant-thread/title.d.ts +4 -1
- package/dist/chat/state/artifacts.d.ts +1 -0
- package/dist/chat/state/conversation.d.ts +0 -1
- package/dist/chat/state/session-log.d.ts +117 -0
- package/dist/chat/state/ttl.d.ts +2 -0
- package/dist/chat/state/turn-session.d.ts +89 -0
- package/dist/chat/tools/advisor/tool.d.ts +2 -0
- package/dist/chat/tools/agent-tools.d.ts +2 -1
- package/dist/chat/tools/skill/call-mcp-tool.d.ts +7 -3
- package/dist/chat/tools/skill/search-mcp-tools.d.ts +15 -3
- package/dist/chat/tools/types.d.ts +0 -1
- package/dist/{chunk-AA5TIFN5.js → chunk-FKEKRBUB.js} +267 -735
- package/dist/{chunk-TTUY467K.js → chunk-H652GMDH.js} +30 -14
- package/dist/chunk-I4FDGMFI.js +950 -0
- package/dist/{chunk-D3G3YOU4.js → chunk-ITOW4DED.js} +1 -1
- package/dist/chunk-QDGD5WVN.js +708 -0
- package/dist/cli/check.js +2 -2
- package/dist/cli/init.js +0 -1
- package/dist/cli/snapshot-warmup.js +5 -3
- package/dist/instrumentation.js +3 -0
- package/dist/reporting.d.ts +113 -0
- package/dist/reporting.js +390 -0
- package/package.json +25 -11
- package/dist/chat/services/turn-checkpoint.d.ts +0 -74
- package/dist/chat/state/pi-session-message-store.d.ts +0 -15
- package/dist/chat/state/turn-session-store.d.ts +0 -49
- package/dist/handlers/diagnostics-dashboard.d.ts +0 -2
- package/dist/handlers/diagnostics.d.ts +0 -2
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStateAdapter,
|
|
3
|
+
toOptionalTrimmed
|
|
4
|
+
} from "./chunk-FKEKRBUB.js";
|
|
5
|
+
import {
|
|
6
|
+
getPluginRuntimeDependencies,
|
|
7
|
+
getPluginRuntimePostinstall,
|
|
8
|
+
withSpan
|
|
9
|
+
} from "./chunk-H652GMDH.js";
|
|
10
|
+
|
|
11
|
+
// src/chat/sandbox/runtime-dependency-snapshots.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
import { Sandbox } from "@vercel/sandbox";
|
|
14
|
+
|
|
15
|
+
// src/chat/sandbox/noninteractive-command.ts
|
|
16
|
+
var NON_INTERACTIVE_ENV = {
|
|
17
|
+
CI: "1",
|
|
18
|
+
TERM: "dumb",
|
|
19
|
+
NO_COLOR: "1",
|
|
20
|
+
PAGER: "cat",
|
|
21
|
+
GIT_PAGER: "cat",
|
|
22
|
+
GH_PROMPT_DISABLED: "1",
|
|
23
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
24
|
+
GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
|
|
25
|
+
GH_SPINNER_DISABLED: "1",
|
|
26
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
27
|
+
GCM_INTERACTIVE: "never",
|
|
28
|
+
DEBIAN_FRONTEND: "noninteractive",
|
|
29
|
+
// Git credential isolation: prevent git from sending its own auth so the
|
|
30
|
+
// sandbox egress proxy's header transforms are the sole credential source.
|
|
31
|
+
GIT_ASKPASS: "/bin/true",
|
|
32
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
33
|
+
GIT_CONFIG_COUNT: "2",
|
|
34
|
+
GIT_CONFIG_KEY_0: "credential.helper",
|
|
35
|
+
GIT_CONFIG_VALUE_0: "",
|
|
36
|
+
GIT_CONFIG_KEY_1: "http.emptyAuth",
|
|
37
|
+
GIT_CONFIG_VALUE_1: "true"
|
|
38
|
+
};
|
|
39
|
+
function shellQuote(value) {
|
|
40
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
41
|
+
}
|
|
42
|
+
function buildEnvExports(options) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
if (options.pathPrefix) {
|
|
45
|
+
lines.push(`export PATH="${options.pathPrefix}"`);
|
|
46
|
+
}
|
|
47
|
+
for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) {
|
|
48
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
49
|
+
}
|
|
50
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
51
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
function toCommandScript(input) {
|
|
56
|
+
return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(
|
|
57
|
+
" "
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
function buildNonInteractiveShellScript(script, options = {}) {
|
|
61
|
+
return [...buildEnvExports(options), "exec </dev/null", script].join(" && ");
|
|
62
|
+
}
|
|
63
|
+
function buildNonInteractiveCommand(input) {
|
|
64
|
+
return {
|
|
65
|
+
cmd: "bash",
|
|
66
|
+
args: [
|
|
67
|
+
input.login ? "-lc" : "-c",
|
|
68
|
+
buildNonInteractiveShellScript(toCommandScript(input), {
|
|
69
|
+
env: input.env,
|
|
70
|
+
pathPrefix: input.pathPrefix
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function runNonInteractiveCommand(sandbox, input) {
|
|
76
|
+
const command = {
|
|
77
|
+
...buildNonInteractiveCommand(input),
|
|
78
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
79
|
+
...input.sudo !== void 0 ? { sudo: input.sudo } : {}
|
|
80
|
+
};
|
|
81
|
+
return await sandbox.runCommand(command);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/chat/sandbox/credentials.ts
|
|
85
|
+
function getVercelSandboxCredentials() {
|
|
86
|
+
const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
|
|
87
|
+
const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
|
|
88
|
+
const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
|
|
89
|
+
if (token && teamId && projectId) {
|
|
90
|
+
return { token, teamId, projectId };
|
|
91
|
+
}
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/chat/sandbox/workspace.ts
|
|
96
|
+
function createSandboxInstance(sandbox) {
|
|
97
|
+
return {
|
|
98
|
+
sandboxId: sandbox.name,
|
|
99
|
+
get sandboxEgressId() {
|
|
100
|
+
return sandbox.currentSession().sessionId;
|
|
101
|
+
},
|
|
102
|
+
fs: sandbox.fs,
|
|
103
|
+
extendTimeout(duration) {
|
|
104
|
+
return sandbox.extendTimeout(duration);
|
|
105
|
+
},
|
|
106
|
+
mkDir(path) {
|
|
107
|
+
return sandbox.mkDir(path);
|
|
108
|
+
},
|
|
109
|
+
readFileToBuffer(input) {
|
|
110
|
+
return sandbox.readFileToBuffer(input);
|
|
111
|
+
},
|
|
112
|
+
runCommand(input) {
|
|
113
|
+
return sandbox.runCommand(input);
|
|
114
|
+
},
|
|
115
|
+
async snapshot() {
|
|
116
|
+
const snapshot = await sandbox.snapshot();
|
|
117
|
+
return { snapshotId: snapshot.snapshotId };
|
|
118
|
+
},
|
|
119
|
+
stop() {
|
|
120
|
+
return sandbox.stop();
|
|
121
|
+
},
|
|
122
|
+
update(params) {
|
|
123
|
+
return sandbox.update(params);
|
|
124
|
+
},
|
|
125
|
+
writeFiles(files) {
|
|
126
|
+
return sandbox.writeFiles(files);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/chat/sandbox/paths.ts
|
|
132
|
+
function normalizeWorkspaceRoot(input) {
|
|
133
|
+
const candidate = (input ?? "").trim();
|
|
134
|
+
if (!candidate) {
|
|
135
|
+
return "/vercel/sandbox";
|
|
136
|
+
}
|
|
137
|
+
const normalized = candidate.replace(/\/+$/, "");
|
|
138
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
139
|
+
}
|
|
140
|
+
var SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(
|
|
141
|
+
process.env.VERCEL_SANDBOX_WORKSPACE_DIR
|
|
142
|
+
);
|
|
143
|
+
var SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
|
|
144
|
+
var SANDBOX_DATA_ROOT = `${SANDBOX_WORKSPACE_ROOT}/data`;
|
|
145
|
+
function sandboxSkillDir(skillName) {
|
|
146
|
+
return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
|
|
147
|
+
}
|
|
148
|
+
function sandboxSkillFile(skillName) {
|
|
149
|
+
return `${sandboxSkillDir(skillName)}/SKILL.md`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/chat/sandbox/runtime-dependency-snapshots.ts
|
|
153
|
+
var SNAPSHOT_CACHE_PREFIX = "junior:sandbox_snapshot_profile";
|
|
154
|
+
var SNAPSHOT_LOCK_PREFIX = "junior:sandbox_snapshot_lock";
|
|
155
|
+
var SNAPSHOT_PROFILE_VERSION = 1;
|
|
156
|
+
var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
157
|
+
var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
|
|
158
|
+
var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
|
|
159
|
+
var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
160
|
+
function sleep(ms) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
setTimeout(resolve, ms);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function profileCacheKey(profileHash) {
|
|
166
|
+
return `${SNAPSHOT_CACHE_PREFIX}:${profileHash}`;
|
|
167
|
+
}
|
|
168
|
+
function profileLockKey(profileHash) {
|
|
169
|
+
return `${SNAPSHOT_LOCK_PREFIX}:${profileHash}`;
|
|
170
|
+
}
|
|
171
|
+
function isExactNpmVersion(version) {
|
|
172
|
+
return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
|
|
173
|
+
}
|
|
174
|
+
function hasFloatingSelector(dep) {
|
|
175
|
+
return dep.type === "npm" && !isExactNpmVersion(dep.version);
|
|
176
|
+
}
|
|
177
|
+
function parseFloatingDepMaxAgeMs() {
|
|
178
|
+
const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
|
|
179
|
+
if (!raw?.trim()) {
|
|
180
|
+
return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
|
|
181
|
+
}
|
|
182
|
+
const parsed = Number.parseInt(raw, 10);
|
|
183
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
184
|
+
return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
function buildDependencyProfile(runtime) {
|
|
189
|
+
const dependencies = getPluginRuntimeDependencies();
|
|
190
|
+
const postinstall = getPluginRuntimePostinstall();
|
|
191
|
+
if (dependencies.length === 0 && postinstall.length === 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
|
|
195
|
+
const hasFloatingVersions = dependencies.some((dep) => hasFloatingSelector(dep)) || postinstall.length > 0;
|
|
196
|
+
const hashInput = JSON.stringify({
|
|
197
|
+
version: SNAPSHOT_PROFILE_VERSION,
|
|
198
|
+
runtime,
|
|
199
|
+
rebuildEpoch,
|
|
200
|
+
dependencies,
|
|
201
|
+
postinstall
|
|
202
|
+
});
|
|
203
|
+
const profileHash = createHash("sha256").update(hashInput).digest("hex");
|
|
204
|
+
return {
|
|
205
|
+
profileHash,
|
|
206
|
+
dependencyCount: dependencies.length,
|
|
207
|
+
hasFloatingVersions,
|
|
208
|
+
dependencies,
|
|
209
|
+
postinstall
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function getRuntimeDependencyProfileHash(runtime) {
|
|
213
|
+
return buildDependencyProfile(runtime)?.profileHash;
|
|
214
|
+
}
|
|
215
|
+
function shouldRebuildCachedSnapshot(profile, cached) {
|
|
216
|
+
if (!profile.hasFloatingVersions) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const maxAgeMs = parseFloatingDepMaxAgeMs();
|
|
220
|
+
if (maxAgeMs === 0) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return Date.now() - cached.createdAtMs > maxAgeMs;
|
|
224
|
+
}
|
|
225
|
+
async function getCachedSnapshot(profileHash) {
|
|
226
|
+
try {
|
|
227
|
+
const state = getStateAdapter();
|
|
228
|
+
await state.connect();
|
|
229
|
+
const raw = await state.get(profileCacheKey(profileHash));
|
|
230
|
+
if (typeof raw !== "string") {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
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") {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return parsed;
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function setCachedSnapshot(entry) {
|
|
243
|
+
const state = getStateAdapter();
|
|
244
|
+
await state.connect();
|
|
245
|
+
await state.set(
|
|
246
|
+
profileCacheKey(entry.profileHash),
|
|
247
|
+
JSON.stringify(entry),
|
|
248
|
+
SNAPSHOT_CACHE_TTL_MS
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
async function withSnapshotSpan(name, op, attributes, callback) {
|
|
252
|
+
return await withSpan(name, op, {}, callback, attributes);
|
|
253
|
+
}
|
|
254
|
+
async function runOrThrow(sandbox, params, label) {
|
|
255
|
+
const result = await runNonInteractiveCommand(sandbox, params);
|
|
256
|
+
if (result.exitCode === 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const stderr = (await result.stderr()).trim();
|
|
260
|
+
const stdout = (await result.stdout()).trim();
|
|
261
|
+
const detail = stderr || stdout || "command failed";
|
|
262
|
+
throw new Error(`${label} failed: ${detail}`);
|
|
263
|
+
}
|
|
264
|
+
async function tryRun(sandbox, params) {
|
|
265
|
+
const result = await runNonInteractiveCommand(sandbox, params);
|
|
266
|
+
if (result.exitCode === 0) {
|
|
267
|
+
return { ok: true };
|
|
268
|
+
}
|
|
269
|
+
const stderr = (await result.stderr()).trim();
|
|
270
|
+
const stdout = (await result.stdout()).trim();
|
|
271
|
+
return { ok: false, detail: stderr || stdout || "command failed" };
|
|
272
|
+
}
|
|
273
|
+
async function installGhCliViaDnf(sandbox) {
|
|
274
|
+
const direct = await tryRun(sandbox, {
|
|
275
|
+
cmd: "dnf",
|
|
276
|
+
args: ["install", "-y", "gh"],
|
|
277
|
+
sudo: true
|
|
278
|
+
});
|
|
279
|
+
if (direct.ok) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const dnf5Repo = await tryRun(sandbox, {
|
|
283
|
+
cmd: "dnf",
|
|
284
|
+
args: [
|
|
285
|
+
"config-manager",
|
|
286
|
+
"addrepo",
|
|
287
|
+
"--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
|
|
288
|
+
],
|
|
289
|
+
sudo: true
|
|
290
|
+
});
|
|
291
|
+
if (!dnf5Repo.ok) {
|
|
292
|
+
await runOrThrow(
|
|
293
|
+
sandbox,
|
|
294
|
+
{
|
|
295
|
+
cmd: "dnf",
|
|
296
|
+
args: ["install", "-y", "dnf-command(config-manager)"],
|
|
297
|
+
sudo: true
|
|
298
|
+
},
|
|
299
|
+
"dnf install dnf-command(config-manager)"
|
|
300
|
+
);
|
|
301
|
+
await runOrThrow(
|
|
302
|
+
sandbox,
|
|
303
|
+
{
|
|
304
|
+
cmd: "dnf",
|
|
305
|
+
args: [
|
|
306
|
+
"config-manager",
|
|
307
|
+
"--add-repo",
|
|
308
|
+
"https://cli.github.com/packages/rpm/gh-cli.repo"
|
|
309
|
+
],
|
|
310
|
+
sudo: true
|
|
311
|
+
},
|
|
312
|
+
"dnf config-manager --add-repo gh-cli.repo"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
await runOrThrow(
|
|
316
|
+
sandbox,
|
|
317
|
+
{
|
|
318
|
+
cmd: "dnf",
|
|
319
|
+
args: ["install", "-y", "gh", "--repo", "gh-cli"],
|
|
320
|
+
sudo: true
|
|
321
|
+
},
|
|
322
|
+
"dnf install gh --repo gh-cli"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
function runtimeDependencyFilePath(url, sha256) {
|
|
326
|
+
let urlBasename = "package.rpm";
|
|
327
|
+
try {
|
|
328
|
+
const pathname = new URL(url).pathname;
|
|
329
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
330
|
+
const candidate = segments[segments.length - 1];
|
|
331
|
+
if (candidate) {
|
|
332
|
+
urlBasename = candidate;
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
const sanitizedBasename = urlBasename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
337
|
+
return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
|
|
338
|
+
}
|
|
339
|
+
async function installRuntimeDependencies(sandbox, deps) {
|
|
340
|
+
const systemDeps = deps.filter(
|
|
341
|
+
(dep) => dep.type === "system"
|
|
342
|
+
);
|
|
343
|
+
const npmPackages = deps.filter(
|
|
344
|
+
(dep) => dep.type === "npm"
|
|
345
|
+
).map((dep) => `${dep.package}@${dep.version}`);
|
|
346
|
+
if (systemDeps.length > 0) {
|
|
347
|
+
await withSnapshotSpan(
|
|
348
|
+
"sandbox.snapshot.install_system",
|
|
349
|
+
"sandbox.snapshot.install.system",
|
|
350
|
+
{
|
|
351
|
+
"app.sandbox.snapshot.install.system_count": systemDeps.length
|
|
352
|
+
},
|
|
353
|
+
async () => {
|
|
354
|
+
for (const dep of systemDeps) {
|
|
355
|
+
if ("url" in dep) {
|
|
356
|
+
const rpmPath = runtimeDependencyFilePath(dep.url, dep.sha256);
|
|
357
|
+
await runOrThrow(
|
|
358
|
+
sandbox,
|
|
359
|
+
{
|
|
360
|
+
cmd: "curl",
|
|
361
|
+
args: ["-fsSL", dep.url, "-o", rpmPath]
|
|
362
|
+
},
|
|
363
|
+
`curl download ${dep.url}`
|
|
364
|
+
);
|
|
365
|
+
const checksumResult = await runNonInteractiveCommand(sandbox, {
|
|
366
|
+
cmd: "sha256sum",
|
|
367
|
+
args: [rpmPath]
|
|
368
|
+
});
|
|
369
|
+
const checksumStdout = (await checksumResult.stdout()).trim();
|
|
370
|
+
const checksumStderr = (await checksumResult.stderr()).trim();
|
|
371
|
+
if (checksumResult.exitCode !== 0) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
|
|
377
|
+
if (!actualChecksum) {
|
|
378
|
+
throw new Error("sha256sum produced empty output");
|
|
379
|
+
}
|
|
380
|
+
if (actualChecksum !== dep.sha256) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`checksum mismatch for ${dep.url}: expected ${dep.sha256}, got ${actualChecksum}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
await runOrThrow(
|
|
386
|
+
sandbox,
|
|
387
|
+
{
|
|
388
|
+
cmd: "dnf",
|
|
389
|
+
args: ["install", "-y", rpmPath],
|
|
390
|
+
sudo: true
|
|
391
|
+
},
|
|
392
|
+
`dnf install ${dep.url}`
|
|
393
|
+
);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (dep.package === "gh") {
|
|
397
|
+
await installGhCliViaDnf(sandbox);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
await runOrThrow(
|
|
401
|
+
sandbox,
|
|
402
|
+
{
|
|
403
|
+
cmd: "dnf",
|
|
404
|
+
args: ["install", "-y", dep.package],
|
|
405
|
+
sudo: true
|
|
406
|
+
},
|
|
407
|
+
`dnf install ${dep.package}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (npmPackages.length > 0) {
|
|
414
|
+
await withSnapshotSpan(
|
|
415
|
+
"sandbox.snapshot.install_npm",
|
|
416
|
+
"sandbox.snapshot.install.npm",
|
|
417
|
+
{
|
|
418
|
+
"app.sandbox.snapshot.install.npm_count": npmPackages.length
|
|
419
|
+
},
|
|
420
|
+
async () => {
|
|
421
|
+
await runOrThrow(
|
|
422
|
+
sandbox,
|
|
423
|
+
{
|
|
424
|
+
cmd: "npm",
|
|
425
|
+
args: [
|
|
426
|
+
"install",
|
|
427
|
+
"--global",
|
|
428
|
+
"--prefix",
|
|
429
|
+
`${SANDBOX_WORKSPACE_ROOT}/.junior`,
|
|
430
|
+
...npmPackages
|
|
431
|
+
]
|
|
432
|
+
},
|
|
433
|
+
"npm install"
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function runRuntimePostinstall(sandbox, commands) {
|
|
440
|
+
if (commands.length === 0) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await withSnapshotSpan(
|
|
444
|
+
"sandbox.snapshot.runtime_postinstall",
|
|
445
|
+
"sandbox.snapshot.runtime_postinstall",
|
|
446
|
+
{
|
|
447
|
+
"app.sandbox.snapshot.runtime_postinstall.count": commands.length
|
|
448
|
+
},
|
|
449
|
+
async () => {
|
|
450
|
+
for (const command of commands) {
|
|
451
|
+
const result = await runNonInteractiveCommand(sandbox, {
|
|
452
|
+
cmd: command.cmd,
|
|
453
|
+
args: command.args,
|
|
454
|
+
login: true,
|
|
455
|
+
pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
|
|
456
|
+
...command.sudo !== void 0 ? { sudo: command.sudo } : {}
|
|
457
|
+
});
|
|
458
|
+
if (result.exitCode === 0) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const stderr = (await result.stderr()).trim();
|
|
462
|
+
const stdout = (await result.stdout()).trim();
|
|
463
|
+
const detail = stderr || stdout || "command failed";
|
|
464
|
+
throw new Error(`runtime-postinstall ${command.cmd} failed: ${detail}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
async function createDependencySnapshot(profile, runtime, timeoutMs) {
|
|
470
|
+
return await withSnapshotSpan(
|
|
471
|
+
"sandbox.snapshot.build",
|
|
472
|
+
"sandbox.snapshot.build",
|
|
473
|
+
{
|
|
474
|
+
"app.sandbox.runtime": runtime,
|
|
475
|
+
"app.sandbox.snapshot.dependency_count": profile.dependencyCount
|
|
476
|
+
},
|
|
477
|
+
async () => {
|
|
478
|
+
const sandboxCredentials = getVercelSandboxCredentials();
|
|
479
|
+
const sandbox = createSandboxInstance(
|
|
480
|
+
await Sandbox.create({
|
|
481
|
+
timeout: timeoutMs,
|
|
482
|
+
runtime,
|
|
483
|
+
...sandboxCredentials ?? {}
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
try {
|
|
487
|
+
await installRuntimeDependencies(sandbox, profile.dependencies);
|
|
488
|
+
await runRuntimePostinstall(sandbox, profile.postinstall);
|
|
489
|
+
return await withSnapshotSpan(
|
|
490
|
+
"sandbox.snapshot.capture",
|
|
491
|
+
"sandbox.snapshot.capture",
|
|
492
|
+
{
|
|
493
|
+
"app.sandbox.snapshot.dependency_count": profile.dependencyCount
|
|
494
|
+
},
|
|
495
|
+
async () => {
|
|
496
|
+
const snapshot = await sandbox.snapshot();
|
|
497
|
+
return snapshot.snapshotId;
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
} finally {
|
|
501
|
+
try {
|
|
502
|
+
await sandbox.stop();
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks) {
|
|
510
|
+
const state = getStateAdapter();
|
|
511
|
+
await state.connect();
|
|
512
|
+
const lockKey = profileLockKey(profileHash);
|
|
513
|
+
const tryAcquireLock = async () => await state.acquireLock(lockKey, SNAPSHOT_BUILD_LOCK_TTL_MS);
|
|
514
|
+
let lock = await tryAcquireLock();
|
|
515
|
+
if (lock) {
|
|
516
|
+
try {
|
|
517
|
+
const result = await callback();
|
|
518
|
+
return {
|
|
519
|
+
snapshotId: result.snapshotId,
|
|
520
|
+
source: result.source,
|
|
521
|
+
waitedForLock: false
|
|
522
|
+
};
|
|
523
|
+
} finally {
|
|
524
|
+
await state.releaseLock(lock);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return await withSnapshotSpan(
|
|
528
|
+
"sandbox.snapshot.lock_wait",
|
|
529
|
+
"sandbox.snapshot.lock_wait",
|
|
530
|
+
{
|
|
531
|
+
"app.sandbox.snapshot.profile_hash": profileHash
|
|
532
|
+
},
|
|
533
|
+
async () => {
|
|
534
|
+
await hooks?.onWaitingForLock?.();
|
|
535
|
+
const waitUntil = Date.now() + SNAPSHOT_WAIT_FOR_LOCK_MS;
|
|
536
|
+
while (Date.now() < waitUntil) {
|
|
537
|
+
const cached2 = await getCachedSnapshot(profileHash);
|
|
538
|
+
if (cached2?.snapshotId && canUseCachedSnapshot(cached2)) {
|
|
539
|
+
return {
|
|
540
|
+
snapshotId: cached2.snapshotId,
|
|
541
|
+
source: "wait_cache",
|
|
542
|
+
waitedForLock: true
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
lock = await tryAcquireLock();
|
|
546
|
+
if (lock) {
|
|
547
|
+
try {
|
|
548
|
+
const result = await callback();
|
|
549
|
+
return {
|
|
550
|
+
snapshotId: result.snapshotId,
|
|
551
|
+
source: result.source,
|
|
552
|
+
waitedForLock: true
|
|
553
|
+
};
|
|
554
|
+
} finally {
|
|
555
|
+
await state.releaseLock(lock);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
await sleep(500);
|
|
559
|
+
}
|
|
560
|
+
const cached = await getCachedSnapshot(profileHash);
|
|
561
|
+
if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
|
|
562
|
+
return {
|
|
563
|
+
snapshotId: cached.snapshotId,
|
|
564
|
+
source: "wait_cache",
|
|
565
|
+
waitedForLock: true
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
throw new Error("Timed out waiting for snapshot build lock");
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
function toResolveOutcome(forceRebuild, source, waitedForLock) {
|
|
573
|
+
if (source === "built") {
|
|
574
|
+
return forceRebuild ? "forced_rebuild" : "rebuilt";
|
|
575
|
+
}
|
|
576
|
+
if (waitedForLock || source === "wait_cache") {
|
|
577
|
+
return "cache_hit_after_lock_wait";
|
|
578
|
+
}
|
|
579
|
+
return "cache_hit";
|
|
580
|
+
}
|
|
581
|
+
function getRebuildReason(params) {
|
|
582
|
+
if (params.forceRebuild) {
|
|
583
|
+
return params.staleSnapshotId ? "snapshot_missing" : "force_rebuild";
|
|
584
|
+
}
|
|
585
|
+
if (params.cached?.snapshotId && params.shouldRebuildCached) {
|
|
586
|
+
return "floating_stale";
|
|
587
|
+
}
|
|
588
|
+
if (!params.cached?.snapshotId) {
|
|
589
|
+
return "cache_miss";
|
|
590
|
+
}
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
async function resolveRuntimeDependencySnapshot(params) {
|
|
594
|
+
return await withSnapshotSpan(
|
|
595
|
+
"sandbox.snapshot.resolve",
|
|
596
|
+
"sandbox.snapshot.resolve",
|
|
597
|
+
{
|
|
598
|
+
"app.sandbox.runtime": params.runtime,
|
|
599
|
+
"app.sandbox.snapshot.force_rebuild": Boolean(params.forceRebuild)
|
|
600
|
+
},
|
|
601
|
+
async () => {
|
|
602
|
+
await params.onProgress?.("resolve_start");
|
|
603
|
+
const resolveStartedAtMs = Date.now();
|
|
604
|
+
const profile = buildDependencyProfile(params.runtime);
|
|
605
|
+
if (!profile) {
|
|
606
|
+
return {
|
|
607
|
+
dependencyCount: 0,
|
|
608
|
+
cacheHit: false,
|
|
609
|
+
resolveOutcome: "no_profile"
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const cached = await getCachedSnapshot(profile.profileHash);
|
|
613
|
+
const cachedNeedsRebuild = Boolean(
|
|
614
|
+
cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
|
|
615
|
+
);
|
|
616
|
+
if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
|
|
617
|
+
await params.onProgress?.("cache_hit");
|
|
618
|
+
return {
|
|
619
|
+
snapshotId: cached.snapshotId,
|
|
620
|
+
profileHash: profile.profileHash,
|
|
621
|
+
dependencyCount: profile.dependencyCount,
|
|
622
|
+
cacheHit: true,
|
|
623
|
+
resolveOutcome: "cache_hit"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const rebuildReason = getRebuildReason({
|
|
627
|
+
forceRebuild: params.forceRebuild,
|
|
628
|
+
staleSnapshotId: params.staleSnapshotId,
|
|
629
|
+
cached,
|
|
630
|
+
shouldRebuildCached: cachedNeedsRebuild
|
|
631
|
+
});
|
|
632
|
+
const canUseCachedSnapshot = (candidate) => {
|
|
633
|
+
if (params.forceRebuild) {
|
|
634
|
+
if (params.staleSnapshotId) {
|
|
635
|
+
return candidate.snapshotId !== params.staleSnapshotId;
|
|
636
|
+
}
|
|
637
|
+
return candidate.createdAtMs > resolveStartedAtMs;
|
|
638
|
+
}
|
|
639
|
+
return !shouldRebuildCachedSnapshot(profile, candidate);
|
|
640
|
+
};
|
|
641
|
+
const lockResult = await withBuildLock(
|
|
642
|
+
profile.profileHash,
|
|
643
|
+
async () => {
|
|
644
|
+
const latest = await getCachedSnapshot(profile.profileHash);
|
|
645
|
+
if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
|
|
646
|
+
await params.onProgress?.("cache_hit");
|
|
647
|
+
return {
|
|
648
|
+
snapshotId: latest.snapshotId,
|
|
649
|
+
source: "callback_cache"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
await params.onProgress?.("building_snapshot");
|
|
653
|
+
const nextSnapshotId = await createDependencySnapshot(
|
|
654
|
+
profile,
|
|
655
|
+
params.runtime,
|
|
656
|
+
params.timeoutMs
|
|
657
|
+
);
|
|
658
|
+
await setCachedSnapshot({
|
|
659
|
+
profileHash: profile.profileHash,
|
|
660
|
+
snapshotId: nextSnapshotId,
|
|
661
|
+
runtime: params.runtime,
|
|
662
|
+
createdAtMs: Date.now(),
|
|
663
|
+
dependencyCount: profile.dependencyCount
|
|
664
|
+
});
|
|
665
|
+
await params.onProgress?.("build_complete");
|
|
666
|
+
return { snapshotId: nextSnapshotId, source: "built" };
|
|
667
|
+
},
|
|
668
|
+
canUseCachedSnapshot,
|
|
669
|
+
{
|
|
670
|
+
onWaitingForLock: async () => {
|
|
671
|
+
await params.onProgress?.("waiting_for_lock");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
return {
|
|
676
|
+
snapshotId: lockResult.snapshotId,
|
|
677
|
+
profileHash: profile.profileHash,
|
|
678
|
+
dependencyCount: profile.dependencyCount,
|
|
679
|
+
cacheHit: lockResult.source !== "built",
|
|
680
|
+
resolveOutcome: toResolveOutcome(
|
|
681
|
+
Boolean(params.forceRebuild),
|
|
682
|
+
lockResult.source,
|
|
683
|
+
lockResult.waitedForLock
|
|
684
|
+
),
|
|
685
|
+
...rebuildReason ? { rebuildReason } : {}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function isSnapshotMissingError(error) {
|
|
691
|
+
const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
692
|
+
return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export {
|
|
696
|
+
SANDBOX_WORKSPACE_ROOT,
|
|
697
|
+
SANDBOX_SKILLS_ROOT,
|
|
698
|
+
SANDBOX_DATA_ROOT,
|
|
699
|
+
sandboxSkillDir,
|
|
700
|
+
sandboxSkillFile,
|
|
701
|
+
buildNonInteractiveShellScript,
|
|
702
|
+
runNonInteractiveCommand,
|
|
703
|
+
getVercelSandboxCredentials,
|
|
704
|
+
createSandboxInstance,
|
|
705
|
+
getRuntimeDependencyProfileHash,
|
|
706
|
+
resolveRuntimeDependencySnapshot,
|
|
707
|
+
isSnapshotMissingError
|
|
708
|
+
};
|