@sentry/junior 0.116.0 → 0.117.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/{agent-hooks-67NFITDH.js → agent-hooks-5IK4LPLP.js} +8 -8
  2. package/dist/api/conversations/detail.d.ts +7 -0
  3. package/dist/api/conversations/event-list.d.ts +7 -0
  4. package/dist/api/conversations/events.d.ts +1 -1
  5. package/dist/api/schema/conversation.d.ts +28 -0
  6. package/dist/api/schema.js +1 -1
  7. package/dist/api.js +25 -12
  8. package/dist/app.js +32 -30
  9. package/dist/{catalog-runtime-CWZY6GTH.js → catalog-runtime-MTUYW3AA.js} +5 -5
  10. package/dist/chat/agent/prompt.d.ts +2 -0
  11. package/dist/chat/agent/request.d.ts +2 -1
  12. package/dist/chat/agent/resume.d.ts +2 -0
  13. package/dist/chat/agent/tools.d.ts +1 -0
  14. package/dist/chat/conversations/history.d.ts +22 -1
  15. package/dist/chat/conversations/projection.d.ts +6 -0
  16. package/dist/chat/mcp/client.d.ts +4 -0
  17. package/dist/chat/mcp/errors.d.ts +22 -1
  18. package/dist/chat/oauth-response.d.ts +21 -0
  19. package/dist/chat/plugins/prompt.d.ts +9 -0
  20. package/dist/chat/plugins/types.d.ts +2 -21
  21. package/dist/chat/sandbox/snapshot/install.d.ts +6 -0
  22. package/dist/chat/sandbox/snapshot/profile.d.ts +14 -0
  23. package/dist/chat/sandbox/snapshot/resolve.d.ts +22 -0
  24. package/dist/chat/sandbox/snapshot/span.d.ts +2 -0
  25. package/dist/chat/services/context-compaction-marker.d.ts +1 -0
  26. package/dist/chat/services/turn-session-record.d.ts +2 -0
  27. package/dist/chat/state/turn-session.d.ts +2 -0
  28. package/dist/chat/tools/query-conversation-events.d.ts +1 -1
  29. package/dist/chat/tools/sandbox/view-image.d.ts +11 -0
  30. package/dist/chat/tools/types.d.ts +7 -0
  31. package/dist/{chunk-OF5FODBL.js → chunk-27KWTFP6.js} +33 -12
  32. package/dist/{chunk-MGH5ADQS.js → chunk-3GE3F4LT.js} +1 -1
  33. package/dist/{chunk-JZPQ5K4V.js → chunk-5O3JDQDG.js} +154 -20
  34. package/dist/{chunk-PGKWXHYJ.js → chunk-6G45D3YG.js} +114 -10
  35. package/dist/{chunk-3JOGUT46.js → chunk-72Z3HNTQ.js} +1 -1
  36. package/dist/{chunk-6RQ5GCWH.js → chunk-AKG4TZNH.js} +2 -2
  37. package/dist/{chunk-TC4ZEM2D.js → chunk-BFIRIZPH.js} +9 -0
  38. package/dist/{chunk-25GU5B5E.js → chunk-CU7HTGNI.js} +1 -1
  39. package/dist/{chunk-PZGPCOSD.js → chunk-DS27ZBSP.js} +4 -2
  40. package/dist/{chunk-3H45VOBZ.js → chunk-FAWBONPC.js} +404 -391
  41. package/dist/{chunk-53ZKTQO6.js → chunk-KU6ALEJC.js} +1 -1
  42. package/dist/{chunk-UEVLIYK5.js → chunk-LN765Y4N.js} +6 -6
  43. package/dist/{chunk-4DD6AOCG.js → chunk-LUMEBOWC.js} +1 -1
  44. package/dist/{chunk-5UNB623T.js → chunk-NHZEUHKH.js} +514 -344
  45. package/dist/{chunk-VZCSLYCE.js → chunk-STUI6NIL.js} +78 -16
  46. package/dist/{chunk-BXLS3I4W.js → chunk-W5JEQQEA.js} +1 -1
  47. package/dist/{chunk-VKZ6WVAY.js → chunk-WEK3V5RI.js} +73 -32
  48. package/dist/{chunk-N7TMYLF2.js → chunk-WUNPRM76.js} +1 -1
  49. package/dist/{chunk-CI3VK7X5.js → chunk-YRWJ3KEU.js} +2 -2
  50. package/dist/{chunk-KBNDFU6J.js → chunk-ZRQUQJUV.js} +12 -2
  51. package/dist/cli/chat.js +21 -21
  52. package/dist/cli/check.js +6 -6
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli/plugins.js +11 -11
  55. package/dist/cli/{snapshot-warmup.js → snapshot-create.js} +17 -19
  56. package/dist/cli/upgrade.js +5 -5
  57. package/dist/{db-57FHVGGQ.js → db-J2ITKS63.js} +5 -5
  58. package/dist/handlers/oauth-html.d.ts +4 -2
  59. package/dist/instrumentation.js +2 -2
  60. package/dist/nitro.js +2 -2
  61. package/dist/{oauth-callback-server-PAQRPXZX.js → oauth-callback-server-NQYKBT24.js} +15 -15
  62. package/dist/{runner-4ISZYJEX.js → runner-53V5GIQM.js} +10 -10
  63. package/dist/{skills-UE2KXQMB.js → skills-BU3ZMLVB.js} +6 -6
  64. package/dist/{validation-PO5LHTJT.js → validation-74EIKKE6.js} +6 -6
  65. package/dist/version.js +1 -1
  66. package/package.json +6 -6
  67. package/dist/chat/sandbox/runtime-dependency-snapshots.d.ts +0 -20
@@ -1,31 +1,138 @@
1
1
  import {
2
2
  SANDBOX_WORKSPACE_ROOT,
3
3
  getStateAdapter
4
- } from "./chunk-53ZKTQO6.js";
4
+ } from "./chunk-KU6ALEJC.js";
5
5
  import {
6
6
  sleep
7
7
  } from "./chunk-4ZNGQH7C.js";
8
8
  import {
9
9
  toOptionalTrimmed
10
- } from "./chunk-PZGPCOSD.js";
10
+ } from "./chunk-DS27ZBSP.js";
11
11
  import {
12
12
  pluginCatalogRuntime
13
- } from "./chunk-25GU5B5E.js";
13
+ } from "./chunk-CU7HTGNI.js";
14
14
  import {
15
15
  setSpanAttributes,
16
16
  setSpanStatus,
17
17
  withSpan
18
- } from "./chunk-3JOGUT46.js";
18
+ } from "./chunk-72Z3HNTQ.js";
19
+
20
+ // src/chat/sandbox/snapshot/profile.ts
21
+ import { createHash } from "crypto";
19
22
 
20
23
  // src/chat/sandbox/runtime-dependencies.ts
21
24
  var GLOBAL_RUNTIME_DEPENDENCIES = [
22
25
  { type: "system", package: "docker" },
26
+ { type: "system", package: "spal-release" },
23
27
  { type: "system", package: "ripgrep" }
24
28
  ];
25
29
 
26
- // src/chat/sandbox/runtime-dependency-snapshots.ts
27
- import { createHash } from "crypto";
30
+ // src/chat/sandbox/snapshot/profile.ts
31
+ var VERSION = 1;
32
+ var DEFAULT_FLOATING_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
33
+ function isExactNpmVersion(version) {
34
+ return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
35
+ }
36
+ function isFloating(dep) {
37
+ return dep.type === "npm" && !isExactNpmVersion(dep.version);
38
+ }
39
+ function mergeDependencies(dependencies2) {
40
+ const seen = /* @__PURE__ */ new Set();
41
+ const npmVersions = /* @__PURE__ */ new Map();
42
+ const merged = [];
43
+ for (const dependency of dependencies2) {
44
+ const key = dependency.type === "npm" ? `${dependency.type}:${dependency.package}:${dependency.version}` : "package" in dependency ? `${dependency.type}:package:${dependency.package}` : `${dependency.type}:url:${dependency.url}:${dependency.sha256}`;
45
+ if (seen.has(key)) {
46
+ continue;
47
+ }
48
+ seen.add(key);
49
+ if (dependency.type === "npm") {
50
+ const version = npmVersions.get(dependency.package);
51
+ if (version !== void 0 && version !== dependency.version) {
52
+ throw new Error(
53
+ `Conflicting runtime dependency versions for ${dependency.package}: ${version} and ${dependency.version}`
54
+ );
55
+ }
56
+ npmVersions.set(dependency.package, dependency.version);
57
+ }
58
+ merged.push(dependency);
59
+ }
60
+ return merged;
61
+ }
62
+ function floatingMaxAgeMs() {
63
+ const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
64
+ if (!raw?.trim()) {
65
+ return DEFAULT_FLOATING_MAX_AGE_MS;
66
+ }
67
+ const parsed = Number.parseInt(raw, 10);
68
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_FLOATING_MAX_AGE_MS;
69
+ }
70
+ function create(runtime) {
71
+ const dependencies2 = mergeDependencies([
72
+ ...GLOBAL_RUNTIME_DEPENDENCIES,
73
+ ...pluginCatalogRuntime.getRuntimeDependencies()
74
+ ]);
75
+ const postinstall2 = pluginCatalogRuntime.getRuntimePostinstall();
76
+ if (dependencies2.length === 0 && postinstall2.length === 0) {
77
+ return null;
78
+ }
79
+ const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
80
+ const floating = dependencies2.some((dependency) => isFloating(dependency)) || postinstall2.length > 0;
81
+ const hash2 = createHash("sha256").update(
82
+ JSON.stringify({
83
+ version: VERSION,
84
+ runtime,
85
+ rebuildEpoch,
86
+ dependencies: dependencies2,
87
+ postinstall: postinstall2
88
+ })
89
+ ).digest("hex");
90
+ return {
91
+ hash: hash2,
92
+ dependencyCount: dependencies2.length,
93
+ floating,
94
+ dependencies: dependencies2,
95
+ postinstall: postinstall2
96
+ };
97
+ }
98
+ function hash(runtime) {
99
+ return create(runtime)?.hash;
100
+ }
101
+ function isStale(profile, createdAtMs) {
102
+ if (!profile.floating) {
103
+ return false;
104
+ }
105
+ const maxAgeMs = floatingMaxAgeMs();
106
+ return maxAgeMs === 0 || Date.now() - createdAtMs > maxAgeMs;
107
+ }
108
+
109
+ // src/chat/sandbox/snapshot/resolve.ts
28
110
  import { Sandbox } from "@vercel/sandbox";
111
+ import { z } from "zod";
112
+
113
+ // src/chat/sandbox/credentials.ts
114
+ function getVercelSandboxCredentials() {
115
+ const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
116
+ const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
117
+ const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
118
+ if (token && teamId && projectId) {
119
+ return { token, teamId, projectId };
120
+ }
121
+ return void 0;
122
+ }
123
+
124
+ // src/chat/sandbox/resources.ts
125
+ function getSandboxResources() {
126
+ const value = process.env.SANDBOX_VCPUS?.trim();
127
+ if (!value || !/^\d+$/.test(value)) {
128
+ return void 0;
129
+ }
130
+ const vcpus = Number(value);
131
+ if (!Number.isSafeInteger(vcpus) || vcpus <= 0) {
132
+ return void 0;
133
+ }
134
+ return { vcpus };
135
+ }
29
136
 
30
137
  // src/chat/sandbox/noninteractive-command.ts
31
138
  var NON_INTERACTIVE_ENV = {
@@ -96,15 +203,215 @@ async function runNonInteractiveCommand(sandbox, input) {
96
203
  return await sandbox.runCommand(command);
97
204
  }
98
205
 
99
- // src/chat/sandbox/credentials.ts
100
- function getVercelSandboxCredentials() {
101
- const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
102
- const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
103
- const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
104
- if (token && teamId && projectId) {
105
- return { token, teamId, projectId };
206
+ // src/chat/sandbox/snapshot/span.ts
207
+ async function trace(name, op, attributes, callback) {
208
+ return await withSpan(name, op, {}, callback, attributes);
209
+ }
210
+
211
+ // src/chat/sandbox/snapshot/install.ts
212
+ async function runOrThrow(sandbox, params, label) {
213
+ const result = await runNonInteractiveCommand(sandbox, params);
214
+ if (result.exitCode === 0) {
215
+ return;
106
216
  }
107
- return void 0;
217
+ const detail = result.stderr.trim() || result.stdout.trim() || "command failed";
218
+ throw new Error(`${label} failed: ${detail}`);
219
+ }
220
+ async function tryRun(sandbox, params) {
221
+ const result = await runNonInteractiveCommand(sandbox, params);
222
+ return result.exitCode === 0;
223
+ }
224
+ async function installGh(sandbox) {
225
+ const installed = await tryRun(sandbox, {
226
+ cmd: "dnf",
227
+ args: ["install", "-y", "gh"],
228
+ sudo: true
229
+ });
230
+ if (installed) {
231
+ return;
232
+ }
233
+ const repoAdded = await tryRun(sandbox, {
234
+ cmd: "dnf",
235
+ args: [
236
+ "config-manager",
237
+ "addrepo",
238
+ "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
239
+ ],
240
+ sudo: true
241
+ });
242
+ if (!repoAdded) {
243
+ await runOrThrow(
244
+ sandbox,
245
+ {
246
+ cmd: "dnf",
247
+ args: ["install", "-y", "dnf-command(config-manager)"],
248
+ sudo: true
249
+ },
250
+ "dnf install dnf-command(config-manager)"
251
+ );
252
+ await runOrThrow(
253
+ sandbox,
254
+ {
255
+ cmd: "dnf",
256
+ args: [
257
+ "config-manager",
258
+ "--add-repo",
259
+ "https://cli.github.com/packages/rpm/gh-cli.repo"
260
+ ],
261
+ sudo: true
262
+ },
263
+ "dnf config-manager --add-repo gh-cli.repo"
264
+ );
265
+ }
266
+ await runOrThrow(
267
+ sandbox,
268
+ {
269
+ cmd: "dnf",
270
+ args: ["install", "-y", "gh", "--repo", "gh-cli"],
271
+ sudo: true
272
+ },
273
+ "dnf install gh --repo gh-cli"
274
+ );
275
+ }
276
+ function filePath(url, sha256) {
277
+ let basename = "package.rpm";
278
+ try {
279
+ const candidate = new URL(url).pathname.split("/").filter(Boolean).at(-1);
280
+ if (candidate) {
281
+ basename = candidate;
282
+ }
283
+ } catch {
284
+ }
285
+ const sanitized = basename.replace(/[^a-zA-Z0-9._-]/g, "_");
286
+ return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitized}`;
287
+ }
288
+ async function installUrl(sandbox, dependency) {
289
+ const rpmPath = filePath(dependency.url, dependency.sha256);
290
+ await runOrThrow(
291
+ sandbox,
292
+ {
293
+ cmd: "curl",
294
+ args: ["-fsSL", dependency.url, "-o", rpmPath]
295
+ },
296
+ `curl download ${dependency.url}`
297
+ );
298
+ const checksum = await runNonInteractiveCommand(sandbox, {
299
+ cmd: "sha256sum",
300
+ args: [rpmPath]
301
+ });
302
+ const expected = dependency.sha256;
303
+ const actual = checksum.stdout.trim().split(/\s+/)[0]?.toLowerCase();
304
+ if (checksum.exitCode !== 0) {
305
+ const detail = checksum.stderr.trim() || checksum.stdout.trim() || "command failed";
306
+ throw new Error(`sha256sum failed: ${detail}`);
307
+ }
308
+ if (!actual) {
309
+ throw new Error("sha256sum produced empty output");
310
+ }
311
+ if (actual !== expected) {
312
+ throw new Error(
313
+ `checksum mismatch for ${dependency.url}: expected ${expected}, got ${actual}`
314
+ );
315
+ }
316
+ await runOrThrow(
317
+ sandbox,
318
+ {
319
+ cmd: "dnf",
320
+ args: ["install", "-y", rpmPath],
321
+ sudo: true
322
+ },
323
+ `dnf install ${dependency.url}`
324
+ );
325
+ }
326
+ async function dependencies(sandbox, values) {
327
+ const system = values.filter(
328
+ (dependency) => dependency.type === "system"
329
+ );
330
+ const npm = values.filter(
331
+ (dependency) => dependency.type === "npm"
332
+ ).map((dependency) => `${dependency.package}@${dependency.version}`);
333
+ if (system.length > 0) {
334
+ await trace(
335
+ "sandbox.snapshot.install_system",
336
+ "sandbox.snapshot.install.system",
337
+ {
338
+ "app.sandbox.snapshot.install.system_count": system.length
339
+ },
340
+ async () => {
341
+ for (const dependency of system) {
342
+ if ("url" in dependency) {
343
+ await installUrl(sandbox, dependency);
344
+ } else if (dependency.package === "gh") {
345
+ await installGh(sandbox);
346
+ } else {
347
+ await runOrThrow(
348
+ sandbox,
349
+ {
350
+ cmd: "dnf",
351
+ args: ["install", "-y", dependency.package],
352
+ sudo: true
353
+ },
354
+ `dnf install ${dependency.package}`
355
+ );
356
+ }
357
+ }
358
+ }
359
+ );
360
+ }
361
+ if (npm.length > 0) {
362
+ await trace(
363
+ "sandbox.snapshot.install_npm",
364
+ "sandbox.snapshot.install.npm",
365
+ {
366
+ "app.sandbox.snapshot.install.npm_count": npm.length
367
+ },
368
+ async () => {
369
+ await runOrThrow(
370
+ sandbox,
371
+ {
372
+ cmd: "npm",
373
+ args: [
374
+ "install",
375
+ "--global",
376
+ "--prefix",
377
+ `${SANDBOX_WORKSPACE_ROOT}/.junior`,
378
+ ...npm
379
+ ]
380
+ },
381
+ "npm install"
382
+ );
383
+ }
384
+ );
385
+ }
386
+ }
387
+ async function postinstall(sandbox, commands) {
388
+ if (commands.length === 0) {
389
+ return;
390
+ }
391
+ await trace(
392
+ "sandbox.snapshot.runtime_postinstall",
393
+ "sandbox.snapshot.runtime_postinstall",
394
+ {
395
+ "app.sandbox.snapshot.runtime_postinstall.count": commands.length
396
+ },
397
+ async () => {
398
+ for (const command of commands) {
399
+ const result = await runNonInteractiveCommand(sandbox, {
400
+ cmd: command.cmd,
401
+ args: command.args,
402
+ login: true,
403
+ pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
404
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
405
+ });
406
+ if (result.exitCode !== 0) {
407
+ const detail = result.stderr.trim() || result.stdout.trim() || "command failed";
408
+ throw new Error(
409
+ `runtime-postinstall ${command.cmd} failed: ${detail}`
410
+ );
411
+ }
412
+ }
413
+ }
414
+ );
108
415
  }
109
416
 
110
417
  // src/chat/sandbox/workspace.ts
@@ -350,21 +657,21 @@ function bindSandboxFileSystem(fs, signal) {
350
657
  return fs;
351
658
  }
352
659
  return {
353
- readFile(filePath, options) {
660
+ readFile(filePath2, options) {
354
661
  signal.throwIfAborted();
355
- return fs.readFile(filePath, { ...options, signal });
662
+ return fs.readFile(filePath2, { ...options, signal });
356
663
  },
357
- writeFile(filePath, content, options) {
664
+ writeFile(filePath2, content, options) {
358
665
  signal.throwIfAborted();
359
- return fs.writeFile(filePath, content, { ...options, signal });
666
+ return fs.writeFile(filePath2, content, { ...options, signal });
360
667
  },
361
- readdir(filePath) {
668
+ readdir(filePath2) {
362
669
  signal.throwIfAborted();
363
- return fs.readdir(filePath, { signal });
670
+ return fs.readdir(filePath2, { signal });
364
671
  },
365
- stat(filePath) {
672
+ stat(filePath2) {
366
673
  signal.throwIfAborted();
367
- return fs.stat(filePath, { signal });
674
+ return fs.stat(filePath2, { signal });
368
675
  }
369
676
  };
370
677
  }
@@ -382,12 +689,12 @@ function createSandboxSession(sandbox, options) {
382
689
  }
383
690
  };
384
691
  const fs = {
385
- readFile: async (filePath, readOptions) => await run(async () => await fileSystem.readFile(filePath, readOptions)),
386
- writeFile: async (filePath, content, writeOptions) => await run(
387
- async () => await fileSystem.writeFile(filePath, content, writeOptions)
692
+ readFile: async (filePath2, readOptions) => await run(async () => await fileSystem.readFile(filePath2, readOptions)),
693
+ writeFile: async (filePath2, content, writeOptions) => await run(
694
+ async () => await fileSystem.writeFile(filePath2, content, writeOptions)
388
695
  ),
389
- readdir: async (filePath, readOptions) => await run(async () => await fileSystem.readdir(filePath, readOptions)),
390
- stat: async (filePath, statOptions) => await run(async () => await fileSystem.stat(filePath, statOptions))
696
+ readdir: async (filePath2, readOptions) => await run(async () => await fileSystem.readdir(filePath2, readOptions)),
697
+ stat: async (filePath2, statOptions) => await run(async () => await fileSystem.stat(filePath2, statOptions))
391
698
  };
392
699
  return {
393
700
  sandboxId: sandbox.name,
@@ -432,106 +739,43 @@ function createSandboxSession(sandbox, options) {
432
739
  };
433
740
  }
434
741
 
435
- // src/chat/sandbox/resources.ts
436
- function getSandboxResources() {
437
- const value = process.env.SANDBOX_VCPUS?.trim();
438
- if (!value || !/^\d+$/.test(value)) {
439
- return void 0;
440
- }
441
- const vcpus = Number(value);
442
- if (!Number.isSafeInteger(vcpus) || vcpus <= 0) {
443
- return void 0;
444
- }
445
- return { vcpus };
446
- }
447
-
448
- // src/chat/sandbox/runtime-dependency-snapshots.ts
742
+ // src/chat/sandbox/snapshot/resolve.ts
449
743
  var SNAPSHOT_CACHE_PREFIX = "junior:sandbox_snapshot_profile";
450
744
  var SNAPSHOT_LOCK_PREFIX = "junior:sandbox_snapshot_lock";
451
- var SNAPSHOT_PROFILE_VERSION = 1;
452
745
  var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
453
- var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
454
- var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
455
- var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
746
+ var SNAPSHOT_BUILD_LOCK_BUFFER_MS = 30 * 1e3;
747
+ var SNAPSHOT_WAIT_FOR_LOCK_BUFFER_MS = 30 * 1e3;
748
+ var cachedSnapshotSchema = z.object({
749
+ profileHash: z.string(),
750
+ snapshotId: z.string(),
751
+ runtime: z.string(),
752
+ createdAtMs: z.number(),
753
+ dependencyCount: z.number()
754
+ }).strict();
456
755
  function profileCacheKey(profileHash) {
457
756
  return `${SNAPSHOT_CACHE_PREFIX}:${profileHash}`;
458
757
  }
459
758
  function profileLockKey(profileHash) {
460
759
  return `${SNAPSHOT_LOCK_PREFIX}:${profileHash}`;
461
760
  }
462
- function isExactNpmVersion(version) {
463
- return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
464
- }
465
- function hasFloatingSelector(dep) {
466
- return dep.type === "npm" && !isExactNpmVersion(dep.version);
467
- }
468
- function parseFloatingDepMaxAgeMs() {
469
- const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
470
- if (!raw?.trim()) {
471
- return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
472
- }
473
- const parsed = Number.parseInt(raw, 10);
474
- if (!Number.isFinite(parsed) || parsed < 0) {
475
- return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
476
- }
477
- return parsed;
478
- }
479
- function buildDependencyProfile(runtime) {
480
- const dependencies = [
481
- ...GLOBAL_RUNTIME_DEPENDENCIES,
482
- ...pluginCatalogRuntime.getRuntimeDependencies()
483
- ];
484
- const postinstall = pluginCatalogRuntime.getRuntimePostinstall();
485
- if (dependencies.length === 0 && postinstall.length === 0) {
761
+ async function getCachedSnapshot(profileHash) {
762
+ const state = getStateAdapter();
763
+ await state.connect();
764
+ const raw = await state.get(profileCacheKey(profileHash));
765
+ if (typeof raw !== "string") {
486
766
  return null;
487
767
  }
488
- const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
489
- const hasFloatingVersions = dependencies.some((dep) => hasFloatingSelector(dep)) || postinstall.length > 0;
490
- const hashInput = JSON.stringify({
491
- version: SNAPSHOT_PROFILE_VERSION,
492
- runtime,
493
- rebuildEpoch,
494
- dependencies,
495
- postinstall
496
- });
497
- const profileHash = createHash("sha256").update(hashInput).digest("hex");
498
- return {
499
- profileHash,
500
- dependencyCount: dependencies.length,
501
- hasFloatingVersions,
502
- dependencies,
503
- postinstall
504
- };
505
- }
506
- function getRuntimeDependencyProfileHash(runtime) {
507
- return buildDependencyProfile(runtime)?.profileHash;
508
- }
509
- function shouldRebuildCachedSnapshot(profile, cached) {
510
- if (!profile.hasFloatingVersions) {
511
- return false;
512
- }
513
- const maxAgeMs = parseFloatingDepMaxAgeMs();
514
- if (maxAgeMs === 0) {
515
- return true;
516
- }
517
- return Date.now() - cached.createdAtMs > maxAgeMs;
518
- }
519
- async function getCachedSnapshot(profileHash) {
768
+ let value;
520
769
  try {
521
- const state = getStateAdapter();
522
- await state.connect();
523
- const raw = await state.get(profileCacheKey(profileHash));
524
- if (typeof raw !== "string") {
525
- return null;
526
- }
527
- const parsed = JSON.parse(raw);
528
- 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") {
529
- return null;
530
- }
531
- return parsed;
770
+ value = JSON.parse(raw);
532
771
  } catch {
533
- return null;
772
+ throw new Error(`Invalid cached sandbox snapshot for ${profileHash}`);
773
+ }
774
+ const parsed = cachedSnapshotSchema.safeParse(value);
775
+ if (!parsed.success || parsed.data.profileHash !== profileHash) {
776
+ throw new Error(`Invalid cached sandbox snapshot for ${profileHash}`);
534
777
  }
778
+ return parsed.data;
535
779
  }
536
780
  async function setCachedSnapshot(entry) {
537
781
  const state = getStateAdapter();
@@ -542,231 +786,13 @@ async function setCachedSnapshot(entry) {
542
786
  SNAPSHOT_CACHE_TTL_MS
543
787
  );
544
788
  }
545
- async function withSnapshotSpan(name, op, attributes, callback) {
546
- return await withSpan(name, op, {}, callback, attributes);
547
- }
548
- async function runOrThrow(sandbox, params, label) {
549
- const result = await runNonInteractiveCommand(sandbox, params);
550
- if (result.exitCode === 0) {
551
- return;
552
- }
553
- const stderr = result.stderr.trim();
554
- const stdout = result.stdout.trim();
555
- const detail = stderr || stdout || "command failed";
556
- throw new Error(`${label} failed: ${detail}`);
557
- }
558
- async function tryRun(sandbox, params) {
559
- const result = await runNonInteractiveCommand(sandbox, params);
560
- if (result.exitCode === 0) {
561
- return { ok: true };
562
- }
563
- const stderr = result.stderr.trim();
564
- const stdout = result.stdout.trim();
565
- return { ok: false, detail: stderr || stdout || "command failed" };
566
- }
567
- async function installGhCliViaDnf(sandbox) {
568
- const direct = await tryRun(sandbox, {
569
- cmd: "dnf",
570
- args: ["install", "-y", "gh"],
571
- sudo: true
572
- });
573
- if (direct.ok) {
574
- return;
575
- }
576
- const dnf5Repo = await tryRun(sandbox, {
577
- cmd: "dnf",
578
- args: [
579
- "config-manager",
580
- "addrepo",
581
- "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
582
- ],
583
- sudo: true
584
- });
585
- if (!dnf5Repo.ok) {
586
- await runOrThrow(
587
- sandbox,
588
- {
589
- cmd: "dnf",
590
- args: ["install", "-y", "dnf-command(config-manager)"],
591
- sudo: true
592
- },
593
- "dnf install dnf-command(config-manager)"
594
- );
595
- await runOrThrow(
596
- sandbox,
597
- {
598
- cmd: "dnf",
599
- args: [
600
- "config-manager",
601
- "--add-repo",
602
- "https://cli.github.com/packages/rpm/gh-cli.repo"
603
- ],
604
- sudo: true
605
- },
606
- "dnf config-manager --add-repo gh-cli.repo"
607
- );
608
- }
609
- await runOrThrow(
610
- sandbox,
611
- {
612
- cmd: "dnf",
613
- args: ["install", "-y", "gh", "--repo", "gh-cli"],
614
- sudo: true
615
- },
616
- "dnf install gh --repo gh-cli"
617
- );
618
- }
619
- function runtimeDependencyFilePath(url, sha256) {
620
- let urlBasename = "package.rpm";
621
- try {
622
- const pathname = new URL(url).pathname;
623
- const segments = pathname.split("/").filter(Boolean);
624
- const candidate = segments[segments.length - 1];
625
- if (candidate) {
626
- urlBasename = candidate;
627
- }
628
- } catch {
629
- }
630
- const sanitizedBasename = urlBasename.replace(/[^a-zA-Z0-9._-]/g, "_");
631
- return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
632
- }
633
- async function installRuntimeDependencies(sandbox, deps) {
634
- const systemDeps = deps.filter(
635
- (dep) => dep.type === "system"
636
- );
637
- const npmPackages = deps.filter(
638
- (dep) => dep.type === "npm"
639
- ).map((dep) => `${dep.package}@${dep.version}`);
640
- if (systemDeps.length > 0) {
641
- await withSnapshotSpan(
642
- "sandbox.snapshot.install_system",
643
- "sandbox.snapshot.install.system",
644
- {
645
- "app.sandbox.snapshot.install.system_count": systemDeps.length
646
- },
647
- async () => {
648
- for (const dep of systemDeps) {
649
- if ("url" in dep) {
650
- const rpmPath = runtimeDependencyFilePath(dep.url, dep.sha256);
651
- await runOrThrow(
652
- sandbox,
653
- {
654
- cmd: "curl",
655
- args: ["-fsSL", dep.url, "-o", rpmPath]
656
- },
657
- `curl download ${dep.url}`
658
- );
659
- const checksumResult = await runNonInteractiveCommand(sandbox, {
660
- cmd: "sha256sum",
661
- args: [rpmPath]
662
- });
663
- const checksumStdout = checksumResult.stdout.trim();
664
- const checksumStderr = checksumResult.stderr.trim();
665
- if (checksumResult.exitCode !== 0) {
666
- throw new Error(
667
- `sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
668
- );
669
- }
670
- const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
671
- if (!actualChecksum) {
672
- throw new Error("sha256sum produced empty output");
673
- }
674
- if (actualChecksum !== dep.sha256) {
675
- throw new Error(
676
- `checksum mismatch for ${dep.url}: expected ${dep.sha256}, got ${actualChecksum}`
677
- );
678
- }
679
- await runOrThrow(
680
- sandbox,
681
- {
682
- cmd: "dnf",
683
- args: ["install", "-y", rpmPath],
684
- sudo: true
685
- },
686
- `dnf install ${dep.url}`
687
- );
688
- continue;
689
- }
690
- if (dep.package === "gh") {
691
- await installGhCliViaDnf(sandbox);
692
- continue;
693
- }
694
- await runOrThrow(
695
- sandbox,
696
- {
697
- cmd: "dnf",
698
- args: ["install", "-y", dep.package],
699
- sudo: true
700
- },
701
- `dnf install ${dep.package}`
702
- );
703
- }
704
- }
705
- );
706
- }
707
- if (npmPackages.length > 0) {
708
- await withSnapshotSpan(
709
- "sandbox.snapshot.install_npm",
710
- "sandbox.snapshot.install.npm",
711
- {
712
- "app.sandbox.snapshot.install.npm_count": npmPackages.length
713
- },
714
- async () => {
715
- await runOrThrow(
716
- sandbox,
717
- {
718
- cmd: "npm",
719
- args: [
720
- "install",
721
- "--global",
722
- "--prefix",
723
- `${SANDBOX_WORKSPACE_ROOT}/.junior`,
724
- ...npmPackages
725
- ]
726
- },
727
- "npm install"
728
- );
729
- }
730
- );
731
- }
732
- }
733
- async function runRuntimePostinstall(sandbox, commands) {
734
- if (commands.length === 0) {
735
- return;
736
- }
737
- await withSnapshotSpan(
738
- "sandbox.snapshot.runtime_postinstall",
739
- "sandbox.snapshot.runtime_postinstall",
740
- {
741
- "app.sandbox.snapshot.runtime_postinstall.count": commands.length
742
- },
743
- async () => {
744
- for (const command of commands) {
745
- const result = await runNonInteractiveCommand(sandbox, {
746
- cmd: command.cmd,
747
- args: command.args,
748
- login: true,
749
- pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
750
- ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
751
- });
752
- if (result.exitCode === 0) {
753
- continue;
754
- }
755
- const stderr = result.stderr.trim();
756
- const stdout = result.stdout.trim();
757
- const detail = stderr || stdout || "command failed";
758
- throw new Error(`runtime-postinstall ${command.cmd} failed: ${detail}`);
759
- }
760
- }
761
- );
762
- }
763
- async function createDependencySnapshot(profile, runtime, timeoutMs) {
764
- return await withSnapshotSpan(
789
+ async function build(value, runtime, timeoutMs) {
790
+ return await trace(
765
791
  "sandbox.snapshot.build",
766
792
  "sandbox.snapshot.build",
767
793
  {
768
794
  "app.sandbox.runtime": runtime,
769
- "app.sandbox.snapshot.dependency_count": profile.dependencyCount
795
+ "app.sandbox.snapshot.dependency_count": value.dependencyCount
770
796
  },
771
797
  async () => {
772
798
  const sandboxCredentials = getVercelSandboxCredentials();
@@ -780,13 +806,13 @@ async function createDependencySnapshot(profile, runtime, timeoutMs) {
780
806
  })
781
807
  );
782
808
  try {
783
- await installRuntimeDependencies(sandbox, profile.dependencies);
784
- await runRuntimePostinstall(sandbox, profile.postinstall);
785
- return await withSnapshotSpan(
809
+ await dependencies(sandbox, value.dependencies);
810
+ await postinstall(sandbox, value.postinstall);
811
+ return await trace(
786
812
  "sandbox.snapshot.capture",
787
813
  "sandbox.snapshot.capture",
788
814
  {
789
- "app.sandbox.snapshot.dependency_count": profile.dependencyCount
815
+ "app.sandbox.snapshot.dependency_count": value.dependencyCount
790
816
  },
791
817
  async () => {
792
818
  const snapshot = await sandbox.snapshot();
@@ -802,40 +828,35 @@ async function createDependencySnapshot(profile, runtime, timeoutMs) {
802
828
  }
803
829
  );
804
830
  }
805
- async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks) {
831
+ async function withBuildLock(profileHash, timeoutMs, callback, canUseCachedSnapshot, onWaitingForLock) {
806
832
  const state = getStateAdapter();
807
833
  await state.connect();
808
834
  const lockKey = profileLockKey(profileHash);
809
- const tryAcquireLock = async () => await state.acquireLock(lockKey, SNAPSHOT_BUILD_LOCK_TTL_MS);
835
+ const lockTtlMs = timeoutMs + SNAPSHOT_BUILD_LOCK_BUFFER_MS;
836
+ const tryAcquireLock = async () => await state.acquireLock(lockKey, lockTtlMs);
810
837
  let lock = await tryAcquireLock();
811
838
  if (lock) {
812
839
  try {
813
- const result = await callback();
814
- return {
815
- snapshotId: result.snapshotId,
816
- source: result.source,
817
- waitedForLock: false
818
- };
840
+ return await callback();
819
841
  } finally {
820
842
  await state.releaseLock(lock);
821
843
  }
822
844
  }
823
- return await withSnapshotSpan(
845
+ return await trace(
824
846
  "sandbox.snapshot.lock_wait",
825
847
  "sandbox.snapshot.lock_wait",
826
848
  {
827
849
  "app.sandbox.snapshot.profile_hash": profileHash
828
850
  },
829
851
  async () => {
830
- await hooks?.onWaitingForLock?.();
831
- const waitUntil = Date.now() + SNAPSHOT_WAIT_FOR_LOCK_MS;
852
+ await onWaitingForLock?.();
853
+ const waitUntil = Date.now() + lockTtlMs + SNAPSHOT_WAIT_FOR_LOCK_BUFFER_MS;
832
854
  while (Date.now() < waitUntil) {
833
855
  const cached2 = await getCachedSnapshot(profileHash);
834
856
  if (cached2?.snapshotId && canUseCachedSnapshot(cached2)) {
835
857
  return {
836
858
  snapshotId: cached2.snapshotId,
837
- source: "wait_cache",
838
- waitedForLock: true
859
+ source: "cache_hit_after_lock_wait"
839
860
  };
840
861
  }
841
862
  lock = await tryAcquireLock();
@@ -844,8 +865,7 @@ async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks)
844
865
  const result = await callback();
845
866
  return {
846
867
  snapshotId: result.snapshotId,
847
- source: result.source,
848
- waitedForLock: true
868
+ source: result.source === "built" ? "built" : "cache_hit_after_lock_wait"
849
869
  };
850
870
  } finally {
851
871
  await state.releaseLock(lock);
@@ -857,22 +877,18 @@ async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks)
857
877
  if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
858
878
  return {
859
879
  snapshotId: cached.snapshotId,
860
- source: "wait_cache",
861
- waitedForLock: true
880
+ source: "cache_hit_after_lock_wait"
862
881
  };
863
882
  }
864
883
  throw new Error("Timed out waiting for snapshot build lock");
865
884
  }
866
885
  );
867
886
  }
868
- function toResolveOutcome(forceRebuild, source, waitedForLock) {
887
+ function toResolveOutcome(forceRebuild, source) {
869
888
  if (source === "built") {
870
889
  return forceRebuild ? "forced_rebuild" : "rebuilt";
871
890
  }
872
- if (waitedForLock || source === "wait_cache") {
873
- return "cache_hit_after_lock_wait";
874
- }
875
- return "cache_hit";
891
+ return source;
876
892
  }
877
893
  function getRebuildReason(params) {
878
894
  if (params.forceRebuild) {
@@ -886,8 +902,8 @@ function getRebuildReason(params) {
886
902
  }
887
903
  return void 0;
888
904
  }
889
- async function resolveRuntimeDependencySnapshot(params) {
890
- return await withSnapshotSpan(
905
+ async function resolve(params) {
906
+ return await trace(
891
907
  "sandbox.snapshot.resolve",
892
908
  "sandbox.snapshot.resolve",
893
909
  {
@@ -896,25 +912,24 @@ async function resolveRuntimeDependencySnapshot(params) {
896
912
  },
897
913
  async () => {
898
914
  await params.onProgress?.("resolve_start");
899
- const resolveStartedAtMs = Date.now();
900
- const profile = buildDependencyProfile(params.runtime);
901
- if (!profile) {
915
+ const currentProfile = create(params.runtime);
916
+ if (!currentProfile) {
902
917
  return {
903
918
  dependencyCount: 0,
904
919
  cacheHit: false,
905
920
  resolveOutcome: "no_profile"
906
921
  };
907
922
  }
908
- const cached = await getCachedSnapshot(profile.profileHash);
923
+ const cached = await getCachedSnapshot(currentProfile.hash);
909
924
  const cachedNeedsRebuild = Boolean(
910
- cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
925
+ cached?.snapshotId && isStale(currentProfile, cached.createdAtMs)
911
926
  );
912
927
  if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
913
928
  await params.onProgress?.("cache_hit");
914
929
  return {
915
930
  snapshotId: cached.snapshotId,
916
- profileHash: profile.profileHash,
917
- dependencyCount: profile.dependencyCount,
931
+ profileHash: currentProfile.hash,
932
+ dependencyCount: currentProfile.dependencyCount,
918
933
  cacheHit: true,
919
934
  resolveOutcome: "cache_hit"
920
935
  };
@@ -930,60 +945,58 @@ async function resolveRuntimeDependencySnapshot(params) {
930
945
  if (params.staleSnapshotId) {
931
946
  return candidate.snapshotId !== params.staleSnapshotId;
932
947
  }
933
- return candidate.createdAtMs > resolveStartedAtMs;
948
+ return candidate.snapshotId !== cached?.snapshotId;
934
949
  }
935
- return !shouldRebuildCachedSnapshot(profile, candidate);
950
+ return !isStale(currentProfile, candidate.createdAtMs);
936
951
  };
937
952
  const lockResult = await withBuildLock(
938
- profile.profileHash,
953
+ currentProfile.hash,
954
+ params.timeoutMs,
939
955
  async () => {
940
- const latest = await getCachedSnapshot(profile.profileHash);
956
+ const latest = await getCachedSnapshot(currentProfile.hash);
941
957
  if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
942
958
  await params.onProgress?.("cache_hit");
943
959
  return {
944
960
  snapshotId: latest.snapshotId,
945
- source: "callback_cache"
961
+ source: "cache_hit"
946
962
  };
947
963
  }
948
964
  await params.onProgress?.("building_snapshot");
949
- const nextSnapshotId = await createDependencySnapshot(
950
- profile,
965
+ const nextSnapshotId = await build(
966
+ currentProfile,
951
967
  params.runtime,
952
968
  params.timeoutMs
953
969
  );
954
970
  await setCachedSnapshot({
955
- profileHash: profile.profileHash,
971
+ profileHash: currentProfile.hash,
956
972
  snapshotId: nextSnapshotId,
957
973
  runtime: params.runtime,
958
974
  createdAtMs: Date.now(),
959
- dependencyCount: profile.dependencyCount
975
+ dependencyCount: currentProfile.dependencyCount
960
976
  });
961
977
  await params.onProgress?.("build_complete");
962
978
  return { snapshotId: nextSnapshotId, source: "built" };
963
979
  },
964
980
  canUseCachedSnapshot,
965
- {
966
- onWaitingForLock: async () => {
967
- await params.onProgress?.("waiting_for_lock");
968
- }
981
+ async () => {
982
+ await params.onProgress?.("waiting_for_lock");
969
983
  }
970
984
  );
971
985
  return {
972
986
  snapshotId: lockResult.snapshotId,
973
- profileHash: profile.profileHash,
974
- dependencyCount: profile.dependencyCount,
987
+ profileHash: currentProfile.hash,
988
+ dependencyCount: currentProfile.dependencyCount,
975
989
  cacheHit: lockResult.source !== "built",
976
990
  resolveOutcome: toResolveOutcome(
977
991
  Boolean(params.forceRebuild),
978
- lockResult.source,
979
- lockResult.waitedForLock
992
+ lockResult.source
980
993
  ),
981
994
  ...rebuildReason ? { rebuildReason } : {}
982
995
  };
983
996
  }
984
997
  );
985
998
  }
986
- function isSnapshotMissingError(error) {
999
+ function isMissingError(error) {
987
1000
  const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
988
1001
  return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
989
1002
  }
@@ -1000,10 +1013,10 @@ export {
1000
1013
  buildNonInteractiveShellScript,
1001
1014
  runNonInteractiveCommand,
1002
1015
  getSandboxResources,
1003
- GLOBAL_RUNTIME_DEPENDENCIES,
1016
+ create,
1017
+ hash,
1004
1018
  bindSandboxFileSystem,
1005
1019
  createSandboxSession,
1006
- getRuntimeDependencyProfileHash,
1007
- resolveRuntimeDependencySnapshot,
1008
- isSnapshotMissingError
1020
+ resolve,
1021
+ isMissingError
1009
1022
  };