@tetsuo-ai/agenc 0.10.0 → 0.13.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.
@@ -0,0 +1,58 @@
1
+ export interface RuntimeArchiveMember {
2
+ readonly path: string;
3
+ readonly type: string;
4
+ readonly bytes: number;
5
+ readonly mode: number;
6
+ readonly contentSha256: string;
7
+ readonly recordSha256: string;
8
+ readonly linkPath?: string;
9
+ }
10
+
11
+ export interface RuntimeArchiveInventory {
12
+ readonly entries: number;
13
+ readonly uncompressedBytes: number;
14
+ }
15
+
16
+ export interface RuntimeArchiveContentInventory extends RuntimeArchiveInventory {
17
+ readonly members: readonly RuntimeArchiveMember[];
18
+ }
19
+
20
+ export interface EmbeddedNodeIdentity {
21
+ readonly schemaVersion: 1;
22
+ readonly nodeVersion: string;
23
+ readonly nodeMajor: number;
24
+ readonly nodeModuleAbi: string;
25
+ readonly nodeApiVersion: string;
26
+ readonly executable: string;
27
+ readonly executableSha256: string;
28
+ readonly executableBytes: number;
29
+ readonly license: string;
30
+ readonly licenseSha256: string;
31
+ readonly licenseBytes: number;
32
+ readonly libatomic?: string;
33
+ readonly libatomicSha256?: string;
34
+ readonly libatomicBytes?: number;
35
+ readonly libatomicLicense?: string;
36
+ readonly libatomicLicenseSha256?: string;
37
+ readonly libatomicLicenseBytes?: number;
38
+ }
39
+
40
+ export interface EmbeddedNodeRuntimeArchiveInventory
41
+ extends RuntimeArchiveContentInventory {
42
+ readonly embeddedNodeIdentity: Readonly<EmbeddedNodeIdentity>;
43
+ }
44
+
45
+ export function validateRuntimeArchive(
46
+ path: string,
47
+ platform?: string,
48
+ ): RuntimeArchiveInventory;
49
+
50
+ export function runtimeArchiveContentInventory(
51
+ path: string,
52
+ platform?: string,
53
+ ): RuntimeArchiveContentInventory;
54
+
55
+ export function validateEmbeddedNodeRuntimeArchive(
56
+ path: string,
57
+ platform?: string,
58
+ ): EmbeddedNodeRuntimeArchiveInventory;
@@ -8,6 +8,8 @@ const BLOCK_SIZE = 512;
8
8
  const MAX_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
9
9
  const MAX_ENTRIES = 200_000;
10
10
  const MAX_SYMLINK_EXPANSIONS = 64;
11
+ const MAX_EMBEDDED_NODE_IDENTITY_BYTES = 16 * 1024;
12
+ const EMBEDDED_NODE_ROOT = "node_modules/.agenc-node";
11
13
  const decoder = new TextDecoder("utf-8", { fatal: true });
12
14
 
13
15
  function field(block, start, length) {
@@ -203,6 +205,8 @@ function inspectRuntimeArchive(
203
205
  path,
204
206
  platform = process.platform === "win32" ? "win" : process.platform,
205
207
  includeContentInventory = false,
208
+ capturedContentLimits,
209
+ capturedContents,
206
210
  ) {
207
211
  const archive = gunzipSync(readFileSync(path), {
208
212
  maxOutputLength: MAX_UNCOMPRESSED_BYTES,
@@ -264,6 +268,20 @@ function inspectRuntimeArchive(
264
268
  .digest("hex");
265
269
  if (type === "2") member.linkPath = pendingPax?.linkpath ?? headerLink;
266
270
  }
271
+ const captureLimit = capturedContentLimits?.get(memberPath);
272
+ if (captureLimit !== undefined) {
273
+ if (size > captureLimit) {
274
+ throw new Error(
275
+ `runtime archive member exceeds its content limit: ${memberPath}`,
276
+ );
277
+ }
278
+ if (type === "0") {
279
+ capturedContents?.set(
280
+ memberPath,
281
+ Buffer.from(archive.subarray(dataStart, dataEnd)),
282
+ );
283
+ }
284
+ }
267
285
  members.push(member);
268
286
  pendingPax = undefined;
269
287
  pendingPaxStart = undefined;
@@ -297,3 +315,235 @@ export function runtimeArchiveContentInventory(
297
315
  ) {
298
316
  return inspectRuntimeArchive(path, platform, true);
299
317
  }
318
+
319
+ function requirePlainIdentity(value) {
320
+ if (
321
+ value === null ||
322
+ typeof value !== "object" ||
323
+ Array.isArray(value) ||
324
+ Object.getPrototypeOf(value) !== Object.prototype
325
+ ) {
326
+ throw new Error("runtime archive private Node identity must be a JSON object");
327
+ }
328
+ return value;
329
+ }
330
+
331
+ function requireExactIdentityKeys(identity, expectedKeys) {
332
+ const actual = Object.keys(identity).sort();
333
+ const expected = [...expectedKeys].sort();
334
+ if (
335
+ actual.length !== expected.length ||
336
+ actual.some((key, index) => key !== expected[index])
337
+ ) {
338
+ throw new Error("runtime archive private Node identity has an invalid schema");
339
+ }
340
+ }
341
+
342
+ function requireIdentityString(identity, key, pattern) {
343
+ const value = identity[key];
344
+ if (typeof value !== "string" || !pattern.test(value)) {
345
+ throw new Error(`runtime archive private Node identity has an invalid ${key}`);
346
+ }
347
+ return value;
348
+ }
349
+
350
+ function requireIdentityBytes(identity, key) {
351
+ const value = identity[key];
352
+ if (!Number.isSafeInteger(value) || value <= 0) {
353
+ throw new Error(`runtime archive private Node identity has an invalid ${key}`);
354
+ }
355
+ return value;
356
+ }
357
+
358
+ function requireIdentityPath(identity, key, expected) {
359
+ if (identity[key] !== expected) {
360
+ throw new Error(
361
+ `runtime archive private Node identity has an invalid ${key} path`,
362
+ );
363
+ }
364
+ }
365
+
366
+ function requireIdentityMember(members, identity, {
367
+ pathKey,
368
+ expectedRelativePath,
369
+ bytesKey,
370
+ sha256Key,
371
+ }) {
372
+ requireIdentityPath(identity, pathKey, expectedRelativePath);
373
+ const memberPath = `${EMBEDDED_NODE_ROOT}/${expectedRelativePath}`;
374
+ const member = members.get(memberPath);
375
+ if (
376
+ member?.type !== "0" ||
377
+ !Number.isSafeInteger(member.bytes) ||
378
+ member.bytes <= 0
379
+ ) {
380
+ throw new Error(`runtime archive is missing private Node file: ${memberPath}`);
381
+ }
382
+ const expectedBytes = requireIdentityBytes(identity, bytesKey);
383
+ if (member.bytes !== expectedBytes) {
384
+ throw new Error(
385
+ `runtime archive private Node ${pathKey} byte size does not match its identity`,
386
+ );
387
+ }
388
+ const expectedSha256 = requireIdentityString(
389
+ identity,
390
+ sha256Key,
391
+ /^[0-9a-f]{64}$/,
392
+ );
393
+ if (member.contentSha256 !== expectedSha256) {
394
+ throw new Error(
395
+ `runtime archive private Node ${pathKey} SHA256 does not match its identity`,
396
+ );
397
+ }
398
+ return member;
399
+ }
400
+
401
+ function parseEmbeddedNodeIdentity(bytes) {
402
+ let source;
403
+ let identity;
404
+ try {
405
+ source = decoder.decode(bytes);
406
+ identity = requirePlainIdentity(JSON.parse(source));
407
+ } catch (error) {
408
+ if (
409
+ error instanceof Error &&
410
+ error.message.startsWith("runtime archive private Node identity")
411
+ ) {
412
+ throw error;
413
+ }
414
+ throw new Error("runtime archive private Node identity is malformed JSON", {
415
+ cause: error,
416
+ });
417
+ }
418
+ // The producer writes deterministic JSON. Requiring that representation also
419
+ // rejects duplicate keys, which JSON.parse alone would silently overwrite.
420
+ if (source !== `${JSON.stringify(identity, null, 2)}\n`) {
421
+ throw new Error(
422
+ "runtime archive private Node identity is not canonical JSON",
423
+ );
424
+ }
425
+ return identity;
426
+ }
427
+
428
+ export function validateEmbeddedNodeRuntimeArchive(
429
+ path,
430
+ platform = process.platform === "win32" ? "win" : process.platform,
431
+ ) {
432
+ const identityPath = `${EMBEDDED_NODE_ROOT}/identity.json`;
433
+ const capturedContents = new Map();
434
+ const inventory = inspectRuntimeArchive(
435
+ path,
436
+ platform,
437
+ true,
438
+ new Map([[identityPath, MAX_EMBEDDED_NODE_IDENTITY_BYTES]]),
439
+ capturedContents,
440
+ );
441
+ const members = new Map(inventory.members.map((member) => [member.path, member]));
442
+ const windows = platform === "win" || platform === "win32";
443
+ const executable = windows ? "node.exe" : "bin/node";
444
+ for (const member of inventory.members) {
445
+ if (
446
+ member.type === "2" &&
447
+ (member.path === EMBEDDED_NODE_ROOT ||
448
+ member.path.startsWith(`${EMBEDDED_NODE_ROOT}/`))
449
+ ) {
450
+ throw new Error(
451
+ `runtime archive private Node subtree contains a symlink: ${member.path}`,
452
+ );
453
+ }
454
+ }
455
+ const identityMember = members.get(identityPath);
456
+ const identityBytes = capturedContents.get(identityPath);
457
+ if (
458
+ identityMember?.type !== "0" ||
459
+ !Number.isSafeInteger(identityMember.bytes) ||
460
+ identityMember.bytes <= 0 ||
461
+ identityBytes === undefined
462
+ ) {
463
+ throw new Error(`runtime archive is missing private Node file: ${identityPath}`);
464
+ }
465
+ const identity = parseEmbeddedNodeIdentity(identityBytes);
466
+ const commonKeys = [
467
+ "schemaVersion",
468
+ "nodeVersion",
469
+ "nodeMajor",
470
+ "nodeModuleAbi",
471
+ "nodeApiVersion",
472
+ "executable",
473
+ "executableSha256",
474
+ "executableBytes",
475
+ "license",
476
+ "licenseSha256",
477
+ "licenseBytes",
478
+ ];
479
+ const linuxKeys = [
480
+ "libatomic",
481
+ "libatomicSha256",
482
+ "libatomicBytes",
483
+ "libatomicLicense",
484
+ "libatomicLicenseSha256",
485
+ "libatomicLicenseBytes",
486
+ ];
487
+ requireExactIdentityKeys(
488
+ identity,
489
+ platform === "linux" ? [...commonKeys, ...linuxKeys] : commonKeys,
490
+ );
491
+ if (identity.schemaVersion !== 1) {
492
+ throw new Error(
493
+ "runtime archive private Node identity has an invalid schemaVersion",
494
+ );
495
+ }
496
+ const nodeVersion = requireIdentityString(
497
+ identity,
498
+ "nodeVersion",
499
+ /^v[1-9][0-9]*\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/,
500
+ );
501
+ if (
502
+ !Number.isSafeInteger(identity.nodeMajor) ||
503
+ identity.nodeMajor <= 0 ||
504
+ nodeVersion.split(".", 1)[0] !== `v${identity.nodeMajor}`
505
+ ) {
506
+ throw new Error(
507
+ "runtime archive private Node identity has an invalid nodeMajor",
508
+ );
509
+ }
510
+ requireIdentityString(identity, "nodeModuleAbi", /^[1-9][0-9]*$/);
511
+ requireIdentityString(identity, "nodeApiVersion", /^[1-9][0-9]*$/);
512
+
513
+ const executableMember = requireIdentityMember(members, identity, {
514
+ pathKey: "executable",
515
+ expectedRelativePath: executable,
516
+ bytesKey: "executableBytes",
517
+ sha256Key: "executableSha256",
518
+ });
519
+ requireIdentityMember(members, identity, {
520
+ pathKey: "license",
521
+ expectedRelativePath: "LICENSE",
522
+ bytesKey: "licenseBytes",
523
+ sha256Key: "licenseSha256",
524
+ });
525
+ if (platform === "linux") {
526
+ requireIdentityMember(members, identity, {
527
+ pathKey: "libatomic",
528
+ expectedRelativePath: "lib/libatomic.so.1",
529
+ bytesKey: "libatomicBytes",
530
+ sha256Key: "libatomicSha256",
531
+ });
532
+ requireIdentityMember(members, identity, {
533
+ pathKey: "libatomicLicense",
534
+ expectedRelativePath: "LIBATOMIC-LICENSE",
535
+ bytesKey: "libatomicLicenseBytes",
536
+ sha256Key: "libatomicLicenseSha256",
537
+ });
538
+ }
539
+ const executablePath = `${EMBEDDED_NODE_ROOT}/${executable}`;
540
+ if ((executableMember.mode & 0o111) === 0) {
541
+ throw new Error(
542
+ `runtime archive private Node executable is not executable: ${executablePath}`,
543
+ );
544
+ }
545
+ return {
546
+ ...inventory,
547
+ embeddedNodeIdentity: Object.freeze(identity),
548
+ };
549
+ }
@@ -44,9 +44,13 @@ import {
44
44
  currentRuntimeCompatibility,
45
45
  runtimeArtifactKey,
46
46
  } from "./runtime-compatibility.mjs";
47
- import { validateRuntimeArchive } from "./runtime-archive.mjs";
47
+ import {
48
+ validateEmbeddedNodeRuntimeArchive,
49
+ validateRuntimeArchive,
50
+ } from "./runtime-archive.mjs";
48
51
  import {
49
52
  canonicalLocalFileUrlToPath,
53
+ FROZEN_LEGACY_RUNTIME_VERSION,
50
54
  MAX_RUNTIME_ARTIFACT_BYTES,
51
55
  MAX_RUNTIME_MANIFEST_BYTES,
52
56
  validateRuntimeReleaseManifest,
@@ -201,31 +205,45 @@ export function selectArtifact(
201
205
  trustMode,
202
206
  expectedRuntimeVersion: trustMode === "official" ? LAUNCHER_VERSION : undefined,
203
207
  });
208
+ const legacy = manifest.runtimeVersion === FROZEN_LEGACY_RUNTIME_VERSION;
204
209
  const matches = manifest.artifacts.filter(
205
210
  (a) =>
206
211
  a.platform === slug.os &&
207
212
  a.arch === slug.arch &&
208
- a.nodeModuleAbi === nodeModuleAbi,
213
+ (!legacy || a.nodeModuleAbi === nodeModuleAbi),
209
214
  );
210
215
  if (matches.length !== 1) {
211
216
  const have = manifest.artifacts
212
217
  .map((a) => `${a.platform}-${a.arch}/abi${a.nodeModuleAbi ?? "?"}`)
213
218
  .join(", ");
219
+ const requested = legacy
220
+ ? `${slug.os}-${slug.arch}/abi${nodeModuleAbi}`
221
+ : `${slug.os}-${slug.arch}`;
214
222
  if (matches.length > 1) {
215
223
  throw new Error(
216
224
  `agenc: runtime manifest contains multiple builds for ` +
217
- `${slug.os}-${slug.arch}/abi${nodeModuleAbi}`,
225
+ requested,
218
226
  );
219
227
  }
220
228
  throw new Error(
221
- `agenc: no runtime build for ${slug.os}-${slug.arch}/abi${nodeModuleAbi} ` +
222
- `(Node ${process.version}; available: ${have || "none"}). ` +
223
- `Use a Node.js version supported by @tetsuo-ai/agenc ${LAUNCHER_VERSION}, ` +
224
- "then reinstall @tetsuo-ai/agenc before retrying; no runtime was downloaded",
229
+ `agenc: no runtime build for ${requested} ` +
230
+ `(available: ${have || "none"}); no runtime was downloaded`,
225
231
  );
226
232
  }
227
233
  const [match] = matches;
228
- return assertArtifactCompatible(match, runtime);
234
+ // Modern artifacts carry the exact Node executable and native ABI they were
235
+ // built for. The npm host only bootstraps the launcher, so its Node
236
+ // major/ABI must never influence artifact selection or compatibility.
237
+ const artifactRuntime = legacy
238
+ ? runtime
239
+ : {
240
+ ...runtime,
241
+ platform: slug.os,
242
+ arch: slug.arch,
243
+ nodeMajor: match.nodeMajor,
244
+ nodeModuleAbi: match.nodeModuleAbi,
245
+ };
246
+ return assertArtifactCompatible(match, artifactRuntime);
229
247
  }
230
248
 
231
249
  // Directory the artifact extracts to, and the runtime bin inside it.
@@ -246,6 +264,30 @@ export function runtimeBinPath(home, version, artifact) {
246
264
  return join(runtimeInstallDir(home, version, artifact), rel);
247
265
  }
248
266
 
267
+ export function runtimeLaunchPaths(
268
+ home,
269
+ version,
270
+ artifact,
271
+ { legacyNodeBin = process.execPath } = {},
272
+ ) {
273
+ const installDir = runtimeInstallDir(home, version, artifact);
274
+ const legacy = artifact?.runtimeVersion === FROZEN_LEGACY_RUNTIME_VERSION;
275
+ const runtimeBin = join(
276
+ installDir,
277
+ artifact?.bins?.agenc ?? "node_modules/@tetsuo-ai/runtime/bin/agenc",
278
+ );
279
+ const nodeBin = legacy || artifact?.bins?.node === undefined
280
+ ? legacyNodeBin
281
+ : join(installDir, artifact.bins.node);
282
+ return {
283
+ runtimeBin,
284
+ nodeBin,
285
+ ...(legacy || artifact?.bins?.nodeLibrary === undefined
286
+ ? {}
287
+ : { nodeLibraryPath: join(installDir, artifact.bins.nodeLibrary) }),
288
+ };
289
+ }
290
+
249
291
  function markerPath(installDir) {
250
292
  return join(installDir, ".agenc-runtime-ok");
251
293
  }
@@ -276,6 +318,32 @@ function strictRelativeRegularFile(root, relativePath) {
276
318
  }
277
319
  }
278
320
 
321
+ function installRequirements(artifact) {
322
+ const legacy = artifact?.runtimeVersion === FROZEN_LEGACY_RUNTIME_VERSION;
323
+ const regularFiles = [
324
+ artifact?.bins?.agenc ?? "node_modules/@tetsuo-ai/runtime/bin/agenc",
325
+ ];
326
+ const executableFiles = [];
327
+ if (!legacy && artifact?.bins?.node !== undefined) {
328
+ regularFiles.push(artifact.bins.node);
329
+ executableFiles.push(artifact.bins.node);
330
+ }
331
+ if (!legacy && artifact?.bins?.nodeLibrary !== undefined) {
332
+ regularFiles.push(`${artifact.bins.nodeLibrary}/libatomic.so.1`);
333
+ }
334
+ return { regularFiles, executableFiles };
335
+ }
336
+
337
+ function installPayloadReady(path, requirements) {
338
+ if (!requirements.regularFiles.every((relativePath) =>
339
+ strictRelativeRegularFile(path, relativePath))) {
340
+ return false;
341
+ }
342
+ return process.platform === "win32" ||
343
+ requirements.executableFiles.every((relativePath) =>
344
+ (lstatSync(join(path, relativePath)).mode & 0o111) !== 0);
345
+ }
346
+
279
347
  function strictMarkerMatches(installDir, expectedSha) {
280
348
  try {
281
349
  const marker = markerPath(installDir);
@@ -304,12 +372,12 @@ export function isInstalled(installDir, expectedSha, expectedBin) {
304
372
  }
305
373
  }
306
374
 
307
- function readyInstallAt(path, expectedSha, binRel) {
375
+ function readyInstallAt(path, expectedSha, requirements) {
308
376
  try {
309
377
  const root = lstatSync(path);
310
378
  return root.isDirectory() &&
311
379
  !root.isSymbolicLink() &&
312
- strictRelativeRegularFile(path, binRel) &&
380
+ installPayloadReady(path, requirements) &&
313
381
  strictMarkerMatches(path, expectedSha);
314
382
  } catch {
315
383
  return false;
@@ -443,21 +511,27 @@ function durableRemoveDirectory(path, parent, hook) {
443
511
  syncDirectory(parent, "sync-parent-after-remove", hook);
444
512
  }
445
513
 
446
- function reconcileInstallState(versionDir, installDir, expectedSha, binRel, durabilityHook) {
514
+ function reconcileInstallState(
515
+ versionDir,
516
+ installDir,
517
+ expectedSha,
518
+ requirements,
519
+ durabilityHook,
520
+ ) {
447
521
  const base = basename(installDir);
448
522
  const entries = readdirSync(versionDir);
449
523
  const readyStages = entries
450
524
  .filter((name) => name.startsWith(`.${base}.install-`))
451
525
  .map((name) => join(versionDir, name))
452
- .filter((path) => readyInstallAt(path, expectedSha, binRel))
526
+ .filter((path) => readyInstallAt(path, expectedSha, requirements))
453
527
  .sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
454
528
  const readyBackups = entries
455
529
  .filter((name) => name.startsWith(`${base}.old-`))
456
530
  .map((name) => join(versionDir, name))
457
- .filter((path) => readyInstallAt(path, expectedSha, binRel))
531
+ .filter((path) => readyInstallAt(path, expectedSha, requirements))
458
532
  .sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
459
533
 
460
- if (!readyInstallAt(installDir, expectedSha, binRel)) {
534
+ if (!readyInstallAt(installDir, expectedSha, requirements)) {
461
535
  // A staged tree is written and marked before the first promotion rename,
462
536
  // so it is the newest intended state. Prefer it over a backup, then use a
463
537
  // verified backup as the fallback. promoteInstall quarantines any invalid
@@ -468,7 +542,7 @@ function reconcileInstallState(versionDir, installDir, expectedSha, binRel, dura
468
542
  promoteInstall(recoveryCandidate, installDir, durabilityHook);
469
543
  }
470
544
  }
471
- const ready = readyInstallAt(installDir, expectedSha, binRel);
545
+ const ready = readyInstallAt(installDir, expectedSha, requirements);
472
546
  // Never destroy crash evidence or the only recoverable tree until a fully
473
547
  // verified canonical installation exists.
474
548
  if (!ready) return false;
@@ -1104,9 +1178,10 @@ function extractTarGz(archive, destDir) {
1104
1178
  }
1105
1179
  }
1106
1180
 
1107
- // Ensure the runtime for `manifest.runtimeVersion` is present; returns the bin
1108
- // path. Idempotent: a verified existing install short-circuits the download.
1109
- export async function ensureRuntime({
1181
+ // Ensure the runtime for `manifest.runtimeVersion` is present and return every
1182
+ // path needed to launch it with the artifact's private Node. Idempotent: a
1183
+ // verified existing install short-circuits the download.
1184
+ export async function ensureRuntimeLaunch({
1110
1185
  env = process.env,
1111
1186
  userHome = homedir(),
1112
1187
  manifest = readManifest(),
@@ -1139,8 +1214,8 @@ export async function ensureRuntime({
1139
1214
  /* ignore */
1140
1215
  }
1141
1216
  const installDir = runtimeInstallDir(home, version, artifact);
1142
- const bin = runtimeBinPath(home, version, artifact);
1143
- const binRel = artifact?.bins?.agenc ?? "node_modules/@tetsuo-ai/runtime/bin/agenc";
1217
+ const launch = runtimeLaunchPaths(home, version, artifact);
1218
+ const requirements = installRequirements(artifact);
1144
1219
  const versionDir = dirname(installDir);
1145
1220
  mkdirSync(versionDir, { recursive: true, mode: 0o700 });
1146
1221
  // Repair every owner-owned segment between the version dir and the home
@@ -1176,9 +1251,9 @@ export async function ensureRuntime({
1176
1251
  const lockPath = `${installDir}.agenc-lock.sqlite`;
1177
1252
 
1178
1253
  if (
1179
- readyInstallAt(installDir, artifact.sha256, binRel) &&
1254
+ readyInstallAt(installDir, artifact.sha256, requirements) &&
1180
1255
  !hasInstallResidue(versionDir, basename(installDir))
1181
- ) return bin;
1256
+ ) return launch;
1182
1257
 
1183
1258
  // Reconcile a prior crash before doing network I/O. A process killed between
1184
1259
  // the two promotion renames leaves a complete backup that can be restored
@@ -1198,10 +1273,10 @@ export async function ensureRuntime({
1198
1273
  versionDir,
1199
1274
  installDir,
1200
1275
  artifact.sha256,
1201
- binRel,
1276
+ requirements,
1202
1277
  durabilityHook,
1203
1278
  )) {
1204
- return bin;
1279
+ return launch;
1205
1280
  }
1206
1281
  // Network I/O must not hold the filesystem lock. Keep the release handle
1207
1282
  // registered until it succeeds so the outer cleanup can retry a failed
@@ -1228,7 +1303,11 @@ export async function ensureRuntime({
1228
1303
  if (archiveIdentity.size !== BigInt(artifact.bytes)) {
1229
1304
  throw new Error("agenc: bounded runtime download violated its byte-count invariant");
1230
1305
  }
1231
- validateRuntimeArchive(tmp, artifact.platform);
1306
+ if (version === FROZEN_LEGACY_RUNTIME_VERSION) {
1307
+ validateRuntimeArchive(tmp, artifact.platform);
1308
+ } else {
1309
+ validateEmbeddedNodeRuntimeArchive(tmp, artifact.platform);
1310
+ }
1232
1311
  assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
1233
1312
  releaseLock = await acquireLock(lockPath, {
1234
1313
  label: "runtime install",
@@ -1239,9 +1318,9 @@ export async function ensureRuntime({
1239
1318
  versionDir,
1240
1319
  installDir,
1241
1320
  artifact.sha256,
1242
- binRel,
1321
+ requirements,
1243
1322
  durabilityHook,
1244
- )) return bin;
1323
+ )) return launch;
1245
1324
  ({ path: stagingDir, identity: stagingDirIdentity } =
1246
1325
  createPrivateTemporaryDirectory(
1247
1326
  versionDir,
@@ -1250,9 +1329,13 @@ export async function ensureRuntime({
1250
1329
  assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
1251
1330
  extractTarGz(tmp, stagingDir);
1252
1331
  assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
1253
- const stagedBin = join(stagingDir, binRel);
1254
- if (!existsSync(stagedBin)) {
1255
- throw new Error(`agenc: runtime extracted but entry missing: ${stagedBin}`);
1332
+ const missingFile = requirements.regularFiles.find((relativePath) =>
1333
+ !strictRelativeRegularFile(stagingDir, relativePath));
1334
+ if (missingFile !== undefined) {
1335
+ throw new Error(`agenc: runtime extracted but entry missing: ${missingFile}`);
1336
+ }
1337
+ if (!installPayloadReady(stagingDir, requirements)) {
1338
+ throw new Error("agenc: runtime extracted but its private Node is not executable");
1256
1339
  }
1257
1340
  durableWriteMarker(markerPath(stagingDir), artifact.sha256, durabilityHook);
1258
1341
  syncExtractedTree(stagingDir, durabilityHook);
@@ -1263,13 +1346,13 @@ export async function ensureRuntime({
1263
1346
  versionDir,
1264
1347
  installDir,
1265
1348
  artifact.sha256,
1266
- binRel,
1349
+ requirements,
1267
1350
  durabilityHook,
1268
1351
  )) {
1269
1352
  throw new Error("agenc: promoted runtime did not satisfy the marker contract");
1270
1353
  }
1271
1354
  log(`agenc: runtime ${version} ready`);
1272
- return bin;
1355
+ return launch;
1273
1356
  } catch (error) {
1274
1357
  operationError = error;
1275
1358
  throw error;
@@ -1307,3 +1390,9 @@ export async function ensureRuntime({
1307
1390
  }
1308
1391
  }
1309
1392
  }
1393
+
1394
+ // Backward-compatible install-only surface used by npm postinstall and callers
1395
+ // that only need the runtime entrypoint.
1396
+ export async function ensureRuntime(options) {
1397
+ return (await ensureRuntimeLaunch(options)).runtimeBin;
1398
+ }