@tetsuo-ai/agenc 0.6.0 → 0.7.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.
@@ -0,0 +1,197 @@
1
+ // Byte-canonical standalone-installer wrapper contract shared by the runtime
2
+ // updater and both embedded installers. Parsing is deliberately full-file:
3
+ // marker substrings must never grant ownership of a user-authored executable.
4
+
5
+ import { isAbsolute } from "node:path";
6
+
7
+ export const GENERATED_WRAPPER_MAX_BYTES = 64 * 1024;
8
+ const POSIX_WRAPPER_SIGNATURE = "Generated by AgenC install.sh";
9
+ const CMD_WRAPPER_SIGNATURE = "Generated by AgenC install.ps1";
10
+ const WRAPPER_METADATA_PREFIX = "AgenC wrapper metadata v1:";
11
+
12
+ function validateValues(kind, values) {
13
+ if (!values || typeof values !== "object") {
14
+ throw new TypeError("wrapper values must be an object");
15
+ }
16
+ for (const label of ["nodeBin", "runtimeBin", "agencHome"]) {
17
+ const value = values[label];
18
+ if (typeof value !== "string") throw new TypeError(`wrapper ${label} must be a string`);
19
+ if (value.includes("\0")) throw new Error(`wrapper ${label} contains NUL`);
20
+ if (kind === "cmd" && /["\r\n]/u.test(value)) {
21
+ throw new Error(`Windows wrapper ${label} contains an unsupported character`);
22
+ }
23
+ }
24
+ if (!isAbsolute(values.agencHome)) {
25
+ throw new Error("wrapper AGENC_HOME must be an absolute path");
26
+ }
27
+ }
28
+
29
+ function metadataFor(values) {
30
+ return Buffer.from(JSON.stringify({
31
+ nodeBin: values.nodeBin,
32
+ runtimeBin: values.runtimeBin,
33
+ agencHome: values.agencHome,
34
+ }), "utf8").toString("base64url");
35
+ }
36
+
37
+ function renderLegacyOomPosixWrapper({ nodeBin, runtimeBin, agencHome }) {
38
+ return [
39
+ "#!/bin/sh",
40
+ `# ${POSIX_WRAPPER_SIGNATURE} — rewritten on every install/upgrade.`,
41
+ `export AGENC_HOME="\${AGENC_HOME:-${agencHome}}"`,
42
+ "# OOM self-diagnosis: have V8 write a heap snapshot from inside the GC when",
43
+ "# the heap nears its limit (reliable even in the end-stage GC stall where JS",
44
+ "# timers starve), into $AGENC_HOME/oom-snapshots. The runtime prunes old",
45
+ "# captures and points at fresh ones on the next startup. User-provided",
46
+ "# NODE_OPTIONS win: ours are prepended, and we skip entirely when the user",
47
+ "# already tunes heap snapshots.",
48
+ 'case " ${NODE_OPTIONS:-} " in',
49
+ " *heapsnapshot-near-heap-limit*) : ;;",
50
+ " *)",
51
+ ' mkdir -p "${AGENC_HOME}/oom-snapshots" 2>/dev/null || :',
52
+ ' NODE_OPTIONS="--heapsnapshot-near-heap-limit=1 --diagnostic-dir=${AGENC_HOME}/oom-snapshots ${NODE_OPTIONS:-}"',
53
+ " export NODE_OPTIONS",
54
+ " ;;",
55
+ "esac",
56
+ `exec "${nodeBin}" "${runtimeBin}" "$@"`,
57
+ "",
58
+ ].join("\n");
59
+ }
60
+
61
+ export function renderGeneratedWrapperContent({ kind, nodeBin, runtimeBin, agencHome }) {
62
+ if (kind !== "posix" && kind !== "cmd") {
63
+ throw new TypeError(`unsupported wrapper kind: ${String(kind)}`);
64
+ }
65
+ const values = { nodeBin, runtimeBin, agencHome };
66
+ validateValues(kind, values);
67
+ const metadata = metadataFor(values);
68
+ if (kind === "cmd") {
69
+ const batch = (value) => value.replaceAll("%", "%%");
70
+ return [
71
+ "@echo off",
72
+ "setlocal DisableDelayedExpansion",
73
+ `rem ${CMD_WRAPPER_SIGNATURE} - rewritten on every install/upgrade.`,
74
+ `rem ${WRAPPER_METADATA_PREFIX} ${metadata}`,
75
+ `if not defined AGENC_HOME set "AGENC_HOME=${batch(agencHome)}"`,
76
+ `"${batch(nodeBin)}" "${batch(runtimeBin)}" %*`,
77
+ "",
78
+ ].join("\r\n");
79
+ }
80
+ const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
81
+ return [
82
+ "#!/bin/sh",
83
+ `# ${POSIX_WRAPPER_SIGNATURE} — rewritten on every install/upgrade.`,
84
+ `# ${WRAPPER_METADATA_PREFIX} ${metadata}`,
85
+ 'if [ -z "${AGENC_HOME:-}" ]; then',
86
+ ` export AGENC_HOME=${quote(agencHome)}`,
87
+ "fi",
88
+ "# Capture one V8 near-heap-limit snapshot unless the operator already configured it.",
89
+ 'case " ${NODE_OPTIONS:-} " in',
90
+ " *heapsnapshot-near-heap-limit*)",
91
+ ` exec ${quote(nodeBin)} ${quote(runtimeBin)} "$@"`,
92
+ " ;;",
93
+ " *)",
94
+ ' mkdir -p "${AGENC_HOME}/oom-snapshots" 2>/dev/null || :',
95
+ ` exec ${quote(nodeBin)} --heapsnapshot-near-heap-limit=1 ` +
96
+ '--diagnostic-dir="${AGENC_HOME}/oom-snapshots" ' +
97
+ `${quote(runtimeBin)} "$@"`,
98
+ " ;;",
99
+ "esac",
100
+ "",
101
+ ].join("\n");
102
+ }
103
+
104
+ function decodeCanonicalMetadata(encoded) {
105
+ try {
106
+ const bytes = Buffer.from(encoded, "base64url");
107
+ if (bytes.toString("base64url") !== encoded) return undefined;
108
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
109
+ const value = JSON.parse(decoded);
110
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
111
+ if (
112
+ Object.keys(value).length !== 3 ||
113
+ typeof value.nodeBin !== "string" ||
114
+ typeof value.runtimeBin !== "string" ||
115
+ typeof value.agencHome !== "string"
116
+ ) return undefined;
117
+ return {
118
+ nodeBin: value.nodeBin,
119
+ runtimeBin: value.runtimeBin,
120
+ agencHome: value.agencHome,
121
+ };
122
+ } catch {
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ function parseModern(path, content) {
128
+ const marker = content.match(
129
+ /^(#|rem) AgenC wrapper metadata v1: ([A-Za-z0-9_-]+)\r?$/mu,
130
+ );
131
+ if (marker === null) return undefined;
132
+ const kind = marker[1] === "rem" ? "cmd" : "posix";
133
+ const values = decodeCanonicalMetadata(marker[2]);
134
+ if (values === undefined) return undefined;
135
+ try {
136
+ const wrapper = { kind, path, ...values };
137
+ return renderGeneratedWrapperContent(wrapper) === content ? wrapper : undefined;
138
+ } catch {
139
+ return undefined;
140
+ }
141
+ }
142
+
143
+ function parseLegacy(path, content) {
144
+ const posix = content.match(
145
+ /^#!\/bin\/sh\n# Generated by AgenC install\.sh — rewritten on every install\/upgrade\.\nexport AGENC_HOME="\$\{AGENC_HOME:-([^}"\n]+)\}"\nexec "([^"\n]+)" "([^"\n]+)" "\$@"\n$/u,
146
+ );
147
+ if (posix !== null) {
148
+ const values = { agencHome: posix[1], nodeBin: posix[2], runtimeBin: posix[3] };
149
+ try {
150
+ validateValues("posix", values);
151
+ return { kind: "posix", path, ...values };
152
+ } catch {
153
+ return undefined;
154
+ }
155
+ }
156
+ // 0.6.2 development main briefly emitted this exact full-file wrapper before
157
+ // activation ownership became canonical. Accepting only a byte-for-byte
158
+ // reconstruction preserves upgrades from that surface without turning the
159
+ // historical marker into a general ownership oracle.
160
+ const oomPosix = content.match(
161
+ /^#!\/bin\/sh\n# Generated by AgenC install\.sh — rewritten on every install\/upgrade\.\nexport AGENC_HOME="\$\{AGENC_HOME:-([^}"\n]+)\}"\n[\s\S]*\nexec "([^"\n]+)" "([^"\n]+)" "\$@"\n$/u,
162
+ );
163
+ if (oomPosix !== null) {
164
+ const values = {
165
+ agencHome: oomPosix[1],
166
+ nodeBin: oomPosix[2],
167
+ runtimeBin: oomPosix[3],
168
+ };
169
+ try {
170
+ validateValues("posix", values);
171
+ if (renderLegacyOomPosixWrapper(values) === content) {
172
+ return { kind: "posix", path, ...values };
173
+ }
174
+ } catch {
175
+ return undefined;
176
+ }
177
+ }
178
+ const cmd = content.match(
179
+ /^@echo off(\r?\n)rem Generated by AgenC install\.ps1 - rewritten on every install\/upgrade\.\1if not defined AGENC_HOME set "AGENC_HOME=([^"\r\n]+)"\1"([^"\r\n]+)" "([^"\r\n]+)" %\*\1$/u,
180
+ );
181
+ if (cmd === null) return undefined;
182
+ const values = { agencHome: cmd[2], nodeBin: cmd[3], runtimeBin: cmd[4] };
183
+ try {
184
+ validateValues("cmd", values);
185
+ return { kind: "cmd", path, ...values };
186
+ } catch {
187
+ return undefined;
188
+ }
189
+ }
190
+
191
+ export function parseGeneratedWrapperContent(path, content) {
192
+ if (
193
+ typeof path !== "string" || !isAbsolute(path) ||
194
+ typeof content !== "string" || Buffer.byteLength(content, "utf8") > GENERATED_WRAPPER_MAX_BYTES
195
+ ) return null;
196
+ return parseModern(path, content) ?? parseLegacy(path, content) ?? null;
197
+ }
@@ -0,0 +1,299 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { posix } from "node:path";
4
+ import { TextDecoder } from "node:util";
5
+ import { gunzipSync } from "node:zlib";
6
+
7
+ const BLOCK_SIZE = 512;
8
+ const MAX_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
9
+ const MAX_ENTRIES = 200_000;
10
+ const MAX_SYMLINK_EXPANSIONS = 64;
11
+ const decoder = new TextDecoder("utf-8", { fatal: true });
12
+
13
+ function field(block, start, length) {
14
+ const bytes = block.subarray(start, start + length);
15
+ const end = bytes.indexOf(0);
16
+ return decoder.decode(end === -1 ? bytes : bytes.subarray(0, end));
17
+ }
18
+
19
+ function octal(block, start, length, label) {
20
+ const raw = field(block, start, length).trim();
21
+ if (!/^[0-7]+$/.test(raw)) throw new Error(`invalid tar ${label}`);
22
+ const value = Number.parseInt(raw, 8);
23
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`invalid tar ${label}`);
24
+ return value;
25
+ }
26
+
27
+ function validateChecksum(block) {
28
+ const expected = octal(block, 148, 8, "checksum");
29
+ let actual = 0;
30
+ for (let index = 0; index < BLOCK_SIZE; index += 1) {
31
+ actual += index >= 148 && index < 156 ? 0x20 : block[index];
32
+ }
33
+ if (actual !== expected) throw new Error("invalid tar header checksum");
34
+ }
35
+
36
+ function parsePax(data) {
37
+ const values = {};
38
+ const seenKeys = new Set();
39
+ let offset = 0;
40
+ while (offset < data.length) {
41
+ const space = data.indexOf(0x20, offset);
42
+ if (space === -1) throw new Error("invalid PAX record length");
43
+ const lengthText = data.subarray(offset, space).toString("ascii");
44
+ if (!/^[1-9][0-9]*$/.test(lengthText)) throw new Error("invalid PAX record length");
45
+ const length = Number(lengthText);
46
+ const end = offset + length;
47
+ if (!Number.isSafeInteger(length) || end > data.length || data[end - 1] !== 0x0a) {
48
+ throw new Error("invalid PAX record boundary");
49
+ }
50
+ const record = decoder.decode(data.subarray(space + 1, end - 1));
51
+ const equals = record.indexOf("=");
52
+ if (equals <= 0) throw new Error("invalid PAX record");
53
+ const key = record.slice(0, equals);
54
+ const value = record.slice(equals + 1);
55
+ if (seenKeys.has(key)) throw new Error(`duplicate PAX key: ${key}`);
56
+ seenKeys.add(key);
57
+ if (key === "path" || key === "linkpath") values[key] = value;
58
+ else if (key === "size") {
59
+ if (!/^(0|[1-9][0-9]*)$/.test(value)) throw new Error("invalid PAX size");
60
+ const size = Number(value);
61
+ if (!Number.isSafeInteger(size) || size > MAX_UNCOMPRESSED_BYTES) {
62
+ throw new Error("invalid PAX size");
63
+ }
64
+ values.size = size;
65
+ } else if (["mtime", "atime", "ctime"].includes(key)) {
66
+ if (!/^(0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(value)) {
67
+ throw new Error(`invalid PAX ${key}`);
68
+ }
69
+ } else throw new Error(`unsupported PAX key: ${key}`);
70
+ offset = end;
71
+ }
72
+ return values;
73
+ }
74
+
75
+ function unsafePortableSegment(part, platform) {
76
+ if (platform !== "win" && platform !== "darwin") return false;
77
+ if (/[. ]$/.test(part)) return true;
78
+ return platform === "win" && (
79
+ part.includes(":") ||
80
+ /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\.|$)/iu.test(part)
81
+ );
82
+ }
83
+
84
+ function validateMemberPath(path, platform, collisionPaths) {
85
+ if (
86
+ path.length === 0 ||
87
+ /[\\\x00-\x1f\x7f]/.test(path) ||
88
+ path.startsWith("/") ||
89
+ /^[A-Za-z]:/.test(path)
90
+ ) {
91
+ throw new Error(`unsafe runtime archive path: ${path || "(empty)"}`);
92
+ }
93
+ const trimmed = path.endsWith("/") ? path.slice(0, -1) : path;
94
+ const parts = trimmed.split("/");
95
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
96
+ throw new Error(`unsafe runtime archive path: ${path}`);
97
+ }
98
+ if (trimmed !== "node_modules" && !trimmed.startsWith("node_modules/")) {
99
+ throw new Error(`runtime archive member is outside node_modules: ${path}`);
100
+ }
101
+ if (platform === "win" || platform === "darwin") {
102
+ let prefix = "";
103
+ for (const part of parts) {
104
+ if (unsafePortableSegment(part, platform)) {
105
+ throw new Error(`unsafe runtime archive path for ${platform}: ${path}`);
106
+ }
107
+ prefix = prefix ? `${prefix}/${part}` : part;
108
+ const collisionKey = prefix.normalize("NFC").toLowerCase();
109
+ const prior = collisionPaths.get(collisionKey);
110
+ if (prior !== undefined && prior !== prefix) {
111
+ throw new Error(`runtime archive has a case/Unicode path collision: ${prior} and ${prefix}`);
112
+ }
113
+ collisionPaths.set(collisionKey, prefix);
114
+ }
115
+ }
116
+ return trimmed;
117
+ }
118
+
119
+ function validateLink(memberPath, linkPath, platform) {
120
+ if (
121
+ linkPath.length === 0 ||
122
+ /[\\\x00-\x1f\x7f]/.test(linkPath) ||
123
+ linkPath.startsWith("/") ||
124
+ /^[A-Za-z]:/.test(linkPath)
125
+ ) {
126
+ throw new Error(`unsafe runtime archive link target: ${linkPath || "(empty)"}`);
127
+ }
128
+ if (
129
+ (platform === "win" || platform === "darwin") &&
130
+ linkPath.split("/").some((part) =>
131
+ part !== "." && part !== ".." && unsafePortableSegment(part, platform))
132
+ ) {
133
+ throw new Error(`unsafe runtime archive link target for ${platform}: ${linkPath}`);
134
+ }
135
+ const resolved = posix.normalize(posix.join(posix.dirname(memberPath), linkPath));
136
+ if (resolved !== "node_modules" && !resolved.startsWith("node_modules/")) {
137
+ throw new Error(`runtime archive link escapes node_modules: ${memberPath} -> ${linkPath}`);
138
+ }
139
+ }
140
+
141
+ function resolveArchiveGraphPath(components, links) {
142
+ const pending = [...components];
143
+ const resolved = [];
144
+ let expansions = 0;
145
+ let steps = 0;
146
+ while (pending.length > 0) {
147
+ if (++steps > MAX_ENTRIES + MAX_SYMLINK_EXPANSIONS) {
148
+ throw new Error("runtime archive symlink resolution is too complex");
149
+ }
150
+ const part = pending.shift() ?? "";
151
+ if (part === "" || part === ".") continue;
152
+ if (part === "..") {
153
+ if (resolved.length === 0) {
154
+ throw new Error("runtime archive symlink graph escapes the extraction root");
155
+ }
156
+ resolved.pop();
157
+ continue;
158
+ }
159
+ resolved.push(part);
160
+ const candidate = resolved.join("/");
161
+ const target = links.get(candidate);
162
+ if (target === undefined) continue;
163
+ expansions += 1;
164
+ if (expansions > MAX_SYMLINK_EXPANSIONS) {
165
+ throw new Error("runtime archive symlink graph contains a cycle or excessive depth");
166
+ }
167
+ resolved.pop();
168
+ pending.unshift(...target.split("/"));
169
+ }
170
+ return resolved.join("/");
171
+ }
172
+
173
+ function assertGraphResultWithinNodeModules(path) {
174
+ if (path !== "node_modules" && !path.startsWith("node_modules/")) {
175
+ throw new Error(`runtime archive symlink graph escapes node_modules: ${path || "(root)"}`);
176
+ }
177
+ }
178
+
179
+ function validateSymlinkGraph(members, links) {
180
+ for (const member of members) {
181
+ if (member.type === "2") {
182
+ const parent = posix.dirname(member.path);
183
+ if (parent !== ".") {
184
+ assertGraphResultWithinNodeModules(
185
+ resolveArchiveGraphPath(parent.split("/"), links),
186
+ );
187
+ }
188
+ const target = links.get(member.path);
189
+ if (target === undefined) throw new Error(`missing runtime archive link target: ${member.path}`);
190
+ assertGraphResultWithinNodeModules(resolveArchiveGraphPath([
191
+ ...(parent === "." ? [] : parent.split("/")),
192
+ ...target.split("/"),
193
+ ], links));
194
+ } else {
195
+ assertGraphResultWithinNodeModules(
196
+ resolveArchiveGraphPath(member.path.split("/"), links),
197
+ );
198
+ }
199
+ }
200
+ }
201
+
202
+ function inspectRuntimeArchive(
203
+ path,
204
+ platform = process.platform === "win32" ? "win" : process.platform,
205
+ includeContentInventory = false,
206
+ ) {
207
+ const archive = gunzipSync(readFileSync(path), {
208
+ maxOutputLength: MAX_UNCOMPRESSED_BYTES,
209
+ });
210
+ let offset = 0;
211
+ let entries = 0;
212
+ let pendingPax;
213
+ const seen = new Set();
214
+ const members = [];
215
+ const links = new Map();
216
+ const collisionPaths = new Map();
217
+ let pendingPaxStart;
218
+ while (offset + BLOCK_SIZE <= archive.length) {
219
+ const recordStart = pendingPaxStart ?? offset;
220
+ const header = archive.subarray(offset, offset + BLOCK_SIZE);
221
+ if (header.every((byte) => byte === 0)) break;
222
+ validateChecksum(header);
223
+ const size = octal(header, 124, 12, "entry size");
224
+ const dataStart = offset + BLOCK_SIZE;
225
+ const dataEnd = dataStart + size;
226
+ if (dataEnd > archive.length) throw new Error("truncated tar entry");
227
+ const type = String.fromCharCode(header[156] || 0x30);
228
+ const prefix = field(header, 345, 155);
229
+ const headerPath = [prefix, field(header, 0, 100)].filter(Boolean).join("/");
230
+ const headerLink = field(header, 157, 100);
231
+ if (type === "x") {
232
+ if (pendingPax !== undefined) throw new Error("stacked PAX headers are not allowed");
233
+ pendingPax = parsePax(archive.subarray(dataStart, dataEnd));
234
+ pendingPaxStart = offset;
235
+ } else {
236
+ if (pendingPax?.size !== undefined && pendingPax.size !== size) {
237
+ throw new Error("PAX size does not match tar header size");
238
+ }
239
+ if (!["0", "5", "2"].includes(type)) {
240
+ throw new Error(`unsupported runtime archive member type: ${type}`);
241
+ }
242
+ const memberPath = validateMemberPath(
243
+ pendingPax?.path ?? headerPath,
244
+ platform,
245
+ collisionPaths,
246
+ );
247
+ if (seen.has(memberPath)) throw new Error(`duplicate runtime archive member: ${memberPath}`);
248
+ seen.add(memberPath);
249
+ if (type === "2") {
250
+ const linkPath = pendingPax?.linkpath ?? headerLink;
251
+ validateLink(memberPath, linkPath, platform);
252
+ links.set(memberPath, linkPath);
253
+ }
254
+ const member = { path: memberPath, type };
255
+ if (includeContentInventory) {
256
+ const recordEnd = dataStart + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
257
+ member.bytes = size;
258
+ member.mode = octal(header, 100, 8, "entry mode");
259
+ member.contentSha256 = createHash("sha256")
260
+ .update(archive.subarray(dataStart, dataEnd))
261
+ .digest("hex");
262
+ member.recordSha256 = createHash("sha256")
263
+ .update(archive.subarray(recordStart, recordEnd))
264
+ .digest("hex");
265
+ if (type === "2") member.linkPath = pendingPax?.linkpath ?? headerLink;
266
+ }
267
+ members.push(member);
268
+ pendingPax = undefined;
269
+ pendingPaxStart = undefined;
270
+ entries += 1;
271
+ if (entries > MAX_ENTRIES) throw new Error("runtime archive has too many entries");
272
+ }
273
+ offset = dataStart + Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
274
+ }
275
+ if (pendingPax !== undefined) throw new Error("orphaned PAX header");
276
+ if (entries === 0 || !seen.has("node_modules")) {
277
+ throw new Error("runtime archive is empty or missing node_modules");
278
+ }
279
+ validateSymlinkGraph(members, links);
280
+ return {
281
+ entries,
282
+ uncompressedBytes: archive.length,
283
+ ...(includeContentInventory ? { members } : {}),
284
+ };
285
+ }
286
+
287
+ export function validateRuntimeArchive(
288
+ path,
289
+ platform = process.platform === "win32" ? "win" : process.platform,
290
+ ) {
291
+ return inspectRuntimeArchive(path, platform, false);
292
+ }
293
+
294
+ export function runtimeArchiveContentInventory(
295
+ path,
296
+ platform = process.platform === "win32" ? "win" : process.platform,
297
+ ) {
298
+ return inspectRuntimeArchive(path, platform, true);
299
+ }
@@ -0,0 +1,141 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { basename } from "node:path";
4
+
5
+ const DOTTED_VERSION = /^\d+\.\d+(?:\.\d+)?$/;
6
+
7
+ export function compareDottedVersions(left, right) {
8
+ const leftParts = left.split(".").map(Number);
9
+ const rightParts = right.split(".").map(Number);
10
+ for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
11
+ const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
12
+ if (difference !== 0) return Math.sign(difference);
13
+ }
14
+ return 0;
15
+ }
16
+
17
+ function maximumSymbolVersion(path, namespace) {
18
+ const bytes = readFileSync(path).toString("latin1");
19
+ const pattern = new RegExp(`\\b${namespace}_(\\d+\\.\\d+(?:\\.\\d+)?)\\b`, "g");
20
+ let maximum;
21
+ for (const match of bytes.matchAll(pattern)) {
22
+ if (maximum === undefined || compareDottedVersions(match[1], maximum) > 0) {
23
+ maximum = match[1];
24
+ }
25
+ }
26
+ return maximum;
27
+ }
28
+
29
+ export function currentRuntimeCompatibility({
30
+ platform = process.platform,
31
+ arch = process.arch,
32
+ nodeMajor = Number(process.versions.node.split(".")[0]),
33
+ nodeModuleAbi = process.versions.modules,
34
+ report = process.report?.getReport(),
35
+ } = {}) {
36
+ const base = { platform: platform === "win32" ? "win" : platform, arch, nodeMajor, nodeModuleAbi };
37
+ if (base.platform === "darwin") {
38
+ const result = spawnSync("/usr/bin/sw_vers", ["-productVersion"], {
39
+ encoding: "utf8",
40
+ });
41
+ return {
42
+ ...base,
43
+ ...(result.status === 0 ? { macosVersion: result.stdout.trim() } : {}),
44
+ };
45
+ }
46
+ if (base.platform !== "linux") return base;
47
+ const glibcVersion = report?.header?.glibcVersionRuntime;
48
+ const libstdcxx = report?.sharedObjects?.find(
49
+ (path) => basename(path).startsWith("libstdc++.so.6"),
50
+ );
51
+ if (typeof glibcVersion !== "string" || !DOTTED_VERSION.test(glibcVersion)) {
52
+ return { ...base, libcFamily: "unknown" };
53
+ }
54
+ if (typeof libstdcxx !== "string") {
55
+ return { ...base, libcFamily: "glibc", glibcVersion };
56
+ }
57
+ return {
58
+ ...base,
59
+ libcFamily: "glibc",
60
+ glibcVersion,
61
+ glibcxxVersion: maximumSymbolVersion(libstdcxx, "GLIBCXX"),
62
+ cxxAbiVersion: maximumSymbolVersion(libstdcxx, "CXXABI"),
63
+ };
64
+ }
65
+
66
+ function requireDotted(value, label) {
67
+ if (typeof value !== "string" || !DOTTED_VERSION.test(value)) {
68
+ throw new Error(`agenc: runtime artifact has invalid ${label}`);
69
+ }
70
+ return value;
71
+ }
72
+
73
+ export function assertArtifactCompatible(artifact, runtime) {
74
+ if (artifact.nodeMajor !== runtime.nodeMajor) {
75
+ throw new Error(
76
+ `agenc: runtime artifact requires Node ${artifact.nodeMajor}.x; current Node is ${runtime.nodeMajor}.x`,
77
+ );
78
+ }
79
+ if (artifact.nodeModuleAbi !== runtime.nodeModuleAbi) {
80
+ throw new Error(
81
+ `agenc: runtime artifact requires native ABI ${artifact.nodeModuleAbi}; current ABI is ${runtime.nodeModuleAbi}`,
82
+ );
83
+ }
84
+ if (runtime.platform === "darwin") {
85
+ const minimum = requireDotted(
86
+ artifact.minimumMacosVersion,
87
+ "minimum macOS version",
88
+ );
89
+ if (
90
+ typeof runtime.macosVersion !== "string" ||
91
+ !DOTTED_VERSION.test(runtime.macosVersion)
92
+ ) {
93
+ throw new Error("agenc: could not determine host macOS compatibility");
94
+ }
95
+ if (compareDottedVersions(runtime.macosVersion, minimum) < 0) {
96
+ throw new Error(
97
+ `agenc: runtime requires macOS ${minimum} or newer; host provides ${runtime.macosVersion}`,
98
+ );
99
+ }
100
+ return artifact;
101
+ }
102
+ if (runtime.platform !== "linux") return artifact;
103
+ if (artifact.libcFamily !== "glibc") {
104
+ throw new Error("agenc: Linux runtime artifact does not declare the required glibc family");
105
+ }
106
+ if (runtime.libcFamily !== "glibc") {
107
+ throw new Error("agenc: Linux runtime requires glibc; musl/unknown libc is unsupported");
108
+ }
109
+ const requirements = [
110
+ [requireDotted(artifact.minimumGlibcVersion, "minimum glibc version"), runtime.glibcVersion, "glibc"],
111
+ [requireDotted(artifact.minimumGlibcxxVersion, "minimum GLIBCXX version"), runtime.glibcxxVersion, "GLIBCXX"],
112
+ [requireDotted(artifact.minimumCxxAbiVersion, "minimum CXXABI version"), runtime.cxxAbiVersion, "CXXABI"],
113
+ ];
114
+ for (const [minimum, current, label] of requirements) {
115
+ if (typeof current !== "string" || !DOTTED_VERSION.test(current)) {
116
+ throw new Error(`agenc: could not determine host ${label} compatibility`);
117
+ }
118
+ if (compareDottedVersions(current, minimum) < 0) {
119
+ throw new Error(
120
+ `agenc: Linux runtime requires ${label} ${minimum} or newer; host provides ${current}`,
121
+ );
122
+ }
123
+ }
124
+ return artifact;
125
+ }
126
+
127
+ export function runtimeArtifactKey(artifact) {
128
+ const platform = artifact?.platform;
129
+ const arch = artifact?.arch;
130
+ const abi = artifact?.nodeModuleAbi;
131
+ if (!/^(linux|darwin|win)$/.test(platform ?? "")) {
132
+ throw new Error(`agenc: invalid runtime platform ${platform ?? "missing"}`);
133
+ }
134
+ if (!/^(x64|arm64)$/.test(arch ?? "") || !/^\d+$/.test(abi ?? "")) {
135
+ throw new Error("agenc: invalid runtime architecture or native module ABI");
136
+ }
137
+ const libc = platform === "linux"
138
+ ? artifact.libcFamily === "glibc" ? "glibc" : "unknown-libc"
139
+ : "native";
140
+ return `${platform}-${arch}-${libc}-node-abi-${abi}`;
141
+ }