@tetsuo-ai/agenc 0.9.2 → 0.11.2
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/generated/agenc-runtime-manifest-v2.json +118 -109
- package/lib/generated-wrapper.d.mts +2 -0
- package/lib/generated-wrapper.mjs +53 -9
- package/lib/runtime-archive.d.mts +58 -0
- package/lib/runtime-archive.mjs +250 -0
- package/lib/runtime-manager.mjs +121 -32
- package/lib/runtime-release-contract.d.mts +14 -1
- package/lib/runtime-release-contract.mjs +56 -1
- package/package.json +3 -2
- package/src/launcher.mjs +104 -12
package/lib/runtime-archive.mjs
CHANGED
|
@@ -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
|
+
}
|
package/lib/runtime-manager.mjs
CHANGED
|
@@ -44,9 +44,13 @@ import {
|
|
|
44
44
|
currentRuntimeCompatibility,
|
|
45
45
|
runtimeArtifactKey,
|
|
46
46
|
} from "./runtime-compatibility.mjs";
|
|
47
|
-
import {
|
|
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
|
-
|
|
225
|
+
requested,
|
|
218
226
|
);
|
|
219
227
|
}
|
|
220
228
|
throw new Error(
|
|
221
|
-
`agenc: no runtime build for ${
|
|
222
|
-
`(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
1108
|
-
// path
|
|
1109
|
-
|
|
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
|
|
1143
|
-
const
|
|
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,
|
|
1254
|
+
readyInstallAt(installDir, artifact.sha256, requirements) &&
|
|
1180
1255
|
!hasInstallResidue(versionDir, basename(installDir))
|
|
1181
|
-
) return
|
|
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
|
-
|
|
1276
|
+
requirements,
|
|
1202
1277
|
durabilityHook,
|
|
1203
1278
|
)) {
|
|
1204
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
1321
|
+
requirements,
|
|
1243
1322
|
durabilityHook,
|
|
1244
|
-
)) return
|
|
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
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -13,7 +13,11 @@ export interface RuntimeReleaseArtifact {
|
|
|
13
13
|
readonly attestationUrl?: string;
|
|
14
14
|
readonly attestationSha256?: string;
|
|
15
15
|
readonly attestationBytes?: number;
|
|
16
|
-
readonly bins?: {
|
|
16
|
+
readonly bins?: {
|
|
17
|
+
readonly agenc?: string;
|
|
18
|
+
readonly node?: string;
|
|
19
|
+
readonly nodeLibrary?: string;
|
|
20
|
+
};
|
|
17
21
|
readonly [key: string]: unknown;
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -32,6 +36,9 @@ export const MAX_RUNTIME_ARTIFACT_BYTES: number;
|
|
|
32
36
|
export const MAX_RUNTIME_ATTESTATION_BYTES: number;
|
|
33
37
|
export const OFFICIAL_RELEASE_REPOSITORY: string;
|
|
34
38
|
export const OFFICIAL_SOURCE_REPOSITORY: string;
|
|
39
|
+
export const FROZEN_LEGACY_RUNTIME_VERSION: "0.7.2";
|
|
40
|
+
export const MINIMUM_PRIVATE_NODE_RUNTIME_VERSION: "0.11.2";
|
|
41
|
+
export const PRIVATE_NODE_BOOTSTRAP_RELEASE_TAG: "agenc-v0.11.2";
|
|
35
42
|
export const OFFICIAL_RELEASE_WORKFLOW: string;
|
|
36
43
|
export const RUNTIME_ATTESTATION_POLICY: Readonly<{
|
|
37
44
|
repository: string;
|
|
@@ -79,6 +86,12 @@ export function canonicalRuntimeArtifactName(
|
|
|
79
86
|
>,
|
|
80
87
|
): string;
|
|
81
88
|
|
|
89
|
+
export function canonicalRuntimeNodeBin(platform: string): string;
|
|
90
|
+
|
|
91
|
+
export function canonicalRuntimeNodeLibrary(platform: string): string | undefined;
|
|
92
|
+
|
|
93
|
+
export function requireSupportedRuntimeVersion(version: string): string;
|
|
94
|
+
|
|
82
95
|
export function canonicalLocalFileUrlToPath(
|
|
83
96
|
value: string,
|
|
84
97
|
platform?: LocalFileUrlPlatform,
|
|
@@ -10,6 +10,10 @@ export const MAX_RUNTIME_ARTIFACT_BYTES = 256 * 1024 * 1024;
|
|
|
10
10
|
export const MAX_RUNTIME_ATTESTATION_BYTES = 4 * 1024 * 1024;
|
|
11
11
|
export const OFFICIAL_RELEASE_REPOSITORY = "tetsuo-ai/agenc-releases";
|
|
12
12
|
export const OFFICIAL_SOURCE_REPOSITORY = "tetsuo-ai/agenc-core";
|
|
13
|
+
export const FROZEN_LEGACY_RUNTIME_VERSION = "0.7.2";
|
|
14
|
+
export const MINIMUM_PRIVATE_NODE_RUNTIME_VERSION = "0.11.2";
|
|
15
|
+
export const PRIVATE_NODE_BOOTSTRAP_RELEASE_TAG =
|
|
16
|
+
`agenc-v${MINIMUM_PRIVATE_NODE_RUNTIME_VERSION}`;
|
|
13
17
|
export const OFFICIAL_RELEASE_WORKFLOW =
|
|
14
18
|
"tetsuo-ai/agenc-core/.github/workflows/release-runtime.yml";
|
|
15
19
|
export const RUNTIME_ATTESTATION_POLICY = Object.freeze({
|
|
@@ -115,6 +119,49 @@ export function canonicalRuntimeArtifactName(manifest, artifact) {
|
|
|
115
119
|
`-node${artifact.nodeMajor}-abi${artifact.nodeModuleAbi}.tar.gz`;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
export function canonicalRuntimeNodeBin(platform) {
|
|
123
|
+
if (platform === "win" || platform === "win32") {
|
|
124
|
+
return "node_modules/.agenc-node/node.exe";
|
|
125
|
+
}
|
|
126
|
+
if (platform === "linux" || platform === "darwin") {
|
|
127
|
+
return "node_modules/.agenc-node/bin/node";
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`agenc: unsupported runtime Node platform ${String(platform)}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function canonicalRuntimeNodeLibrary(platform) {
|
|
133
|
+
if (platform === "linux") {
|
|
134
|
+
return "node_modules/.agenc-node/lib";
|
|
135
|
+
}
|
|
136
|
+
if (platform === "darwin" || platform === "win" || platform === "win32") {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`agenc: unsupported runtime Node platform ${String(platform)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function requireSupportedRuntimeVersion(version) {
|
|
143
|
+
if (version === FROZEN_LEGACY_RUNTIME_VERSION) {
|
|
144
|
+
return version;
|
|
145
|
+
}
|
|
146
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version ?? "");
|
|
147
|
+
if (match === null) {
|
|
148
|
+
throw new Error("agenc: runtime version is not a canonical semantic version");
|
|
149
|
+
}
|
|
150
|
+
const actual = match.slice(1, 4).map(Number);
|
|
151
|
+
const minimum = MINIMUM_PRIVATE_NODE_RUNTIME_VERSION.split(".").map(Number);
|
|
152
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
153
|
+
if (actual[index] > minimum[index]) return version;
|
|
154
|
+
if (actual[index] < minimum[index]) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`agenc: runtime ${version} has no supported standalone activation contract; ` +
|
|
157
|
+
`use the frozen ${FROZEN_LEGACY_RUNTIME_VERSION} bridge with host Node 25.9, ` +
|
|
158
|
+
`or ${MINIMUM_PRIVATE_NODE_RUNTIME_VERSION} and newer with private Node`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return version;
|
|
163
|
+
}
|
|
164
|
+
|
|
118
165
|
function localFileUrlUsesWindowsPaths(platform) {
|
|
119
166
|
if (platform === "win" || platform === "win32") return true;
|
|
120
167
|
if (
|
|
@@ -289,6 +336,7 @@ export function validateRuntimeReleaseManifest(
|
|
|
289
336
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.runtimeVersion ?? "")) {
|
|
290
337
|
throw new Error("agenc: runtime manifest has an invalid runtimeVersion");
|
|
291
338
|
}
|
|
339
|
+
requireSupportedRuntimeVersion(manifest.runtimeVersion);
|
|
292
340
|
if (manifest.releaseTag !== `agenc-v${manifest.runtimeVersion}`) {
|
|
293
341
|
throw new Error("agenc: runtime manifest releaseTag does not match runtimeVersion");
|
|
294
342
|
}
|
|
@@ -345,7 +393,14 @@ export function validateRuntimeReleaseManifest(
|
|
|
345
393
|
!Number.isSafeInteger(artifact.bytes) ||
|
|
346
394
|
artifact.bytes <= 0 ||
|
|
347
395
|
artifact.bytes > MAX_RUNTIME_ARTIFACT_BYTES ||
|
|
348
|
-
artifact.bins?.agenc !== "node_modules/@tetsuo-ai/runtime/bin/agenc"
|
|
396
|
+
artifact.bins?.agenc !== "node_modules/@tetsuo-ai/runtime/bin/agenc" ||
|
|
397
|
+
(
|
|
398
|
+
manifest.runtimeVersion !== FROZEN_LEGACY_RUNTIME_VERSION &&
|
|
399
|
+
(
|
|
400
|
+
artifact.bins?.node !== canonicalRuntimeNodeBin(artifact.platform) ||
|
|
401
|
+
artifact.bins?.nodeLibrary !== canonicalRuntimeNodeLibrary(artifact.platform)
|
|
402
|
+
)
|
|
403
|
+
)
|
|
349
404
|
) {
|
|
350
405
|
throw new Error(`agenc: runtime manifest artifact identity is invalid (${identity})`);
|
|
351
406
|
}
|