@tetsuo-ai/agenc 0.6.1 → 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.
- package/generated/agenc-runtime-manifest-v2.json +335 -0
- package/lib/activation-lock-identity.d.mts +3 -0
- package/lib/activation-lock-identity.mjs +151 -0
- package/lib/generated-wrapper.d.mts +23 -0
- package/lib/generated-wrapper.mjs +197 -0
- package/lib/runtime-archive.mjs +299 -0
- package/lib/runtime-compatibility.mjs +141 -0
- package/lib/runtime-manager.mjs +1161 -59
- package/lib/runtime-release-contract.d.mts +94 -0
- package/lib/runtime-release-contract.mjs +386 -0
- package/lib/sqlite-lock.d.mts +44 -0
- package/lib/sqlite-lock.mjs +1121 -0
- package/package.json +27 -7
- package/generated/.gitkeep +0 -0
- package/generated/agenc-runtime-manifest.json +0 -63
- package/scripts/build-runtime-tarball.mjs +0 -201
- package/scripts/gen-manifest.mjs +0 -91
package/lib/runtime-manager.mjs
CHANGED
|
@@ -10,36 +10,109 @@
|
|
|
10
10
|
// `postinstall` before anything else is available.
|
|
11
11
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
|
-
import { createHash } from "node:crypto";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
14
|
import {
|
|
15
15
|
chmodSync,
|
|
16
|
+
closeSync,
|
|
17
|
+
constants as fsConstants,
|
|
16
18
|
createReadStream,
|
|
17
19
|
createWriteStream,
|
|
18
20
|
existsSync,
|
|
21
|
+
fstatSync,
|
|
22
|
+
fsyncSync,
|
|
23
|
+
lstatSync,
|
|
19
24
|
mkdirSync,
|
|
25
|
+
mkdtempSync,
|
|
26
|
+
openSync,
|
|
27
|
+
readSync,
|
|
20
28
|
readFileSync,
|
|
29
|
+
readdirSync,
|
|
30
|
+
realpathSync,
|
|
31
|
+
renameSync,
|
|
21
32
|
rmSync,
|
|
22
|
-
|
|
33
|
+
statSync,
|
|
34
|
+
writeSync,
|
|
23
35
|
} from "node:fs";
|
|
24
|
-
import { homedir
|
|
25
|
-
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import { homedir } from "node:os";
|
|
37
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, win32 } from "node:path";
|
|
38
|
+
import { performance } from "node:perf_hooks";
|
|
26
39
|
import { Readable } from "node:stream";
|
|
27
40
|
import { pipeline } from "node:stream/promises";
|
|
28
41
|
import { fileURLToPath } from "node:url";
|
|
42
|
+
import {
|
|
43
|
+
assertArtifactCompatible,
|
|
44
|
+
currentRuntimeCompatibility,
|
|
45
|
+
runtimeArtifactKey,
|
|
46
|
+
} from "./runtime-compatibility.mjs";
|
|
47
|
+
import { validateRuntimeArchive } from "./runtime-archive.mjs";
|
|
48
|
+
import {
|
|
49
|
+
canonicalLocalFileUrlToPath,
|
|
50
|
+
MAX_RUNTIME_ARTIFACT_BYTES,
|
|
51
|
+
MAX_RUNTIME_MANIFEST_BYTES,
|
|
52
|
+
validateRuntimeReleaseManifest,
|
|
53
|
+
} from "./runtime-release-contract.mjs";
|
|
54
|
+
import { acquireLocalSqliteLock } from "./sqlite-lock.mjs";
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
MAX_RUNTIME_ARTIFACT_BYTES,
|
|
58
|
+
MAX_RUNTIME_MANIFEST_BYTES,
|
|
59
|
+
} from "./runtime-release-contract.mjs";
|
|
29
60
|
|
|
30
61
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const LAUNCHER_VERSION = JSON.parse(
|
|
63
|
+
readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
|
|
64
|
+
).version;
|
|
31
65
|
const MANIFEST_PATH = resolve(
|
|
32
66
|
__dirname,
|
|
33
67
|
"..",
|
|
34
68
|
"generated",
|
|
35
|
-
"agenc-runtime-manifest.json",
|
|
69
|
+
"agenc-runtime-manifest-v2.json",
|
|
36
70
|
);
|
|
71
|
+
const DEFAULT_RUNTIME_FETCH_TIMEOUT_MS = 120_000;
|
|
72
|
+
const DEFAULT_RUNTIME_EXTRACTION_TIMEOUT_MS = 120_000;
|
|
73
|
+
const WINDOWS_SYSTEM_ROOT = String.raw`\\?\GLOBALROOT\SystemRoot`;
|
|
37
74
|
|
|
38
75
|
export function resolveAgenCHome(env = process.env, userHome = homedir()) {
|
|
39
|
-
const configured = env.AGENC_HOME
|
|
40
|
-
|
|
76
|
+
const configured = env.AGENC_HOME;
|
|
77
|
+
const requested = configured && configured.length > 0
|
|
41
78
|
? configured
|
|
42
79
|
: join(userHome, ".agenc");
|
|
80
|
+
if (!isAbsolute(requested)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"agenc: AGENC_HOME must be an absolute path so its identity does not change with the working directory",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return resolve(requested);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function canonicalizeAgenCHome(requested) {
|
|
89
|
+
if (!isAbsolute(requested)) {
|
|
90
|
+
throw new Error("agenc: AGENC_HOME must be an absolute path");
|
|
91
|
+
}
|
|
92
|
+
const existed = existsSync(requested);
|
|
93
|
+
mkdirSync(requested, { recursive: true, mode: 0o700 });
|
|
94
|
+
const requestedStat = lstatSync(requested);
|
|
95
|
+
if (!requestedStat.isDirectory() && !requestedStat.isSymbolicLink()) {
|
|
96
|
+
throw new Error(`agenc: AGENC_HOME is not a directory: ${requested}`);
|
|
97
|
+
}
|
|
98
|
+
// An existing symlink alias is deliberately resolved once. All subsequent
|
|
99
|
+
// install paths and lock identities use the canonical target, so changing
|
|
100
|
+
// the alias cannot move an in-flight install into another home.
|
|
101
|
+
if (!existed && requestedStat.isSymbolicLink()) {
|
|
102
|
+
throw new Error(`agenc: newly created AGENC_HOME became a symlink: ${requested}`);
|
|
103
|
+
}
|
|
104
|
+
const canonical = realpathSync(requested);
|
|
105
|
+
const canonicalStat = lstatSync(canonical);
|
|
106
|
+
if (!canonicalStat.isDirectory() || canonicalStat.isSymbolicLink()) {
|
|
107
|
+
throw new Error(`agenc: canonical AGENC_HOME is not a real directory: ${canonical}`);
|
|
108
|
+
}
|
|
109
|
+
if (process.platform !== "win32" && typeof process.getuid === "function") {
|
|
110
|
+
if (canonicalStat.uid !== process.getuid()) {
|
|
111
|
+
throw new Error(`agenc: AGENC_HOME is owned by another user: ${canonical}`);
|
|
112
|
+
}
|
|
113
|
+
chmodSync(canonical, 0o700);
|
|
114
|
+
}
|
|
115
|
+
return canonical;
|
|
43
116
|
}
|
|
44
117
|
|
|
45
118
|
export function platformSlug(platform = process.platform, arch = process.arch) {
|
|
@@ -47,82 +120,987 @@ export function platformSlug(platform = process.platform, arch = process.arch) {
|
|
|
47
120
|
return { os, arch };
|
|
48
121
|
}
|
|
49
122
|
|
|
50
|
-
|
|
123
|
+
function readBoundedManifest(path, maximumBytes) {
|
|
124
|
+
const before = lstatSync(path);
|
|
125
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
126
|
+
throw new Error(`agenc: runtime manifest must be a plain file (${path})`);
|
|
127
|
+
}
|
|
128
|
+
if (before.size > maximumBytes) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`agenc: runtime manifest exceeds ${maximumBytes} bytes (${before.size})`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const fd = openSync(path, "r");
|
|
134
|
+
try {
|
|
135
|
+
const opened = fstatSync(fd);
|
|
136
|
+
if (!opened.isFile() || opened.size > maximumBytes) {
|
|
137
|
+
throw new Error(`agenc: runtime manifest exceeds ${maximumBytes} bytes`);
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
process.platform !== "win32" &&
|
|
141
|
+
(opened.dev !== before.dev || opened.ino !== before.ino)
|
|
142
|
+
) {
|
|
143
|
+
throw new Error("agenc: runtime manifest changed while it was opened");
|
|
144
|
+
}
|
|
145
|
+
const bytes = Buffer.allocUnsafe(maximumBytes + 1);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
while (offset < bytes.length) {
|
|
148
|
+
const count = readSync(fd, bytes, offset, bytes.length - offset, null);
|
|
149
|
+
if (count === 0) break;
|
|
150
|
+
offset += count;
|
|
151
|
+
}
|
|
152
|
+
if (offset > maximumBytes) {
|
|
153
|
+
throw new Error(`agenc: runtime manifest exceeds ${maximumBytes} bytes`);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset));
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error("agenc: runtime manifest is not valid UTF-8");
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function readManifest(
|
|
166
|
+
manifestPath = MANIFEST_PATH,
|
|
167
|
+
maximumBytes = MAX_RUNTIME_MANIFEST_BYTES,
|
|
168
|
+
) {
|
|
51
169
|
if (!existsSync(manifestPath)) {
|
|
52
170
|
throw new Error(
|
|
53
171
|
`agenc: runtime manifest missing (${manifestPath}); this launcher build is incomplete`,
|
|
54
172
|
);
|
|
55
173
|
}
|
|
56
|
-
|
|
174
|
+
if (
|
|
175
|
+
!Number.isSafeInteger(maximumBytes) ||
|
|
176
|
+
maximumBytes < 1 ||
|
|
177
|
+
maximumBytes > MAX_RUNTIME_MANIFEST_BYTES
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError("agenc: runtime manifest byte limit is invalid");
|
|
180
|
+
}
|
|
181
|
+
const source = readBoundedManifest(manifestPath, maximumBytes);
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(source);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw new Error("agenc: runtime manifest is not valid JSON", { cause: error });
|
|
186
|
+
}
|
|
57
187
|
}
|
|
58
188
|
|
|
59
|
-
export function selectArtifact(
|
|
60
|
-
|
|
61
|
-
|
|
189
|
+
export function selectArtifact(
|
|
190
|
+
manifest,
|
|
191
|
+
slug = platformSlug(),
|
|
192
|
+
nodeModuleAbi = process.versions.modules,
|
|
193
|
+
runtime = currentRuntimeCompatibility({
|
|
194
|
+
platform: slug.os,
|
|
195
|
+
arch: slug.arch,
|
|
196
|
+
nodeModuleAbi,
|
|
197
|
+
}),
|
|
198
|
+
{ trustMode = "official" } = {},
|
|
199
|
+
) {
|
|
200
|
+
validateRuntimeReleaseManifest(manifest, {
|
|
201
|
+
trustMode,
|
|
202
|
+
expectedRuntimeVersion: trustMode === "official" ? LAUNCHER_VERSION : undefined,
|
|
203
|
+
});
|
|
204
|
+
const matches = manifest.artifacts.filter(
|
|
205
|
+
(a) =>
|
|
206
|
+
a.platform === slug.os &&
|
|
207
|
+
a.arch === slug.arch &&
|
|
208
|
+
a.nodeModuleAbi === nodeModuleAbi,
|
|
62
209
|
);
|
|
63
|
-
if (
|
|
64
|
-
const have =
|
|
65
|
-
.map((a) => `${a.platform}-${a.arch}`)
|
|
210
|
+
if (matches.length !== 1) {
|
|
211
|
+
const have = manifest.artifacts
|
|
212
|
+
.map((a) => `${a.platform}-${a.arch}/abi${a.nodeModuleAbi ?? "?"}`)
|
|
66
213
|
.join(", ");
|
|
214
|
+
if (matches.length > 1) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`agenc: runtime manifest contains multiple builds for ` +
|
|
217
|
+
`${slug.os}-${slug.arch}/abi${nodeModuleAbi}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
67
220
|
throw new Error(
|
|
68
|
-
`agenc: no runtime build for ${slug.os}-${slug.arch}
|
|
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",
|
|
69
225
|
);
|
|
70
226
|
}
|
|
71
|
-
|
|
227
|
+
const [match] = matches;
|
|
228
|
+
return assertArtifactCompatible(match, runtime);
|
|
72
229
|
}
|
|
73
230
|
|
|
74
231
|
// Directory the artifact extracts to, and the runtime bin inside it.
|
|
75
|
-
export function runtimeInstallDir(
|
|
76
|
-
|
|
232
|
+
export function runtimeInstallDir(
|
|
233
|
+
home,
|
|
234
|
+
version,
|
|
235
|
+
artifact,
|
|
236
|
+
) {
|
|
237
|
+
return join(
|
|
238
|
+
home,
|
|
239
|
+
"runtime",
|
|
240
|
+
version,
|
|
241
|
+
`${runtimeArtifactKey(artifact)}-sha256-${artifact.sha256}`,
|
|
242
|
+
);
|
|
77
243
|
}
|
|
78
244
|
export function runtimeBinPath(home, version, artifact) {
|
|
79
245
|
const rel = artifact?.bins?.agenc ?? "node_modules/@tetsuo-ai/runtime/bin/agenc";
|
|
80
|
-
return join(runtimeInstallDir(home, version), rel);
|
|
246
|
+
return join(runtimeInstallDir(home, version, artifact), rel);
|
|
81
247
|
}
|
|
82
248
|
|
|
83
249
|
function markerPath(installDir) {
|
|
84
250
|
return join(installDir, ".agenc-runtime-ok");
|
|
85
251
|
}
|
|
86
252
|
|
|
253
|
+
function strictRelativeRegularFile(root, relativePath) {
|
|
254
|
+
if (
|
|
255
|
+
relativePath.length === 0 ||
|
|
256
|
+
isAbsolute(relativePath) ||
|
|
257
|
+
relativePath.split(/[\\/]/).some((part) => part.length === 0 || part === "." || part === "..")
|
|
258
|
+
) return false;
|
|
259
|
+
const finalPath = resolve(root, relativePath);
|
|
260
|
+
const within = relative(resolve(root), finalPath);
|
|
261
|
+
if (within === "" || within === ".." || within.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(within)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
let current = root;
|
|
265
|
+
const parts = relativePath.split(/[\\/]/);
|
|
266
|
+
try {
|
|
267
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
268
|
+
current = join(current, parts[index]);
|
|
269
|
+
const stat = lstatSync(current);
|
|
270
|
+
if (stat.isSymbolicLink()) return false;
|
|
271
|
+
if (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()) return false;
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function strictMarkerMatches(installDir, expectedSha) {
|
|
280
|
+
try {
|
|
281
|
+
const marker = markerPath(installDir);
|
|
282
|
+
const stat = lstatSync(marker);
|
|
283
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128) return false;
|
|
284
|
+
const content = readFileSync(marker, "utf8");
|
|
285
|
+
return content === expectedSha || content === `${expectedSha}\n`;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
87
291
|
// A runtime install is "good" only if the success marker exists AND records the
|
|
88
292
|
// expected sha256 — guards against a half-extracted or stale tree.
|
|
89
|
-
export function isInstalled(installDir, expectedSha) {
|
|
90
|
-
|
|
91
|
-
|
|
293
|
+
export function isInstalled(installDir, expectedSha, expectedBin) {
|
|
294
|
+
try {
|
|
295
|
+
const root = lstatSync(installDir);
|
|
296
|
+
if (!root.isDirectory() || root.isSymbolicLink()) return false;
|
|
297
|
+
if (!strictMarkerMatches(installDir, expectedSha)) return false;
|
|
298
|
+
return expectedBin === undefined || strictRelativeRegularFile(
|
|
299
|
+
installDir,
|
|
300
|
+
relative(installDir, expectedBin),
|
|
301
|
+
);
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function readyInstallAt(path, expectedSha, binRel) {
|
|
92
308
|
try {
|
|
93
|
-
|
|
309
|
+
const root = lstatSync(path);
|
|
310
|
+
return root.isDirectory() &&
|
|
311
|
+
!root.isSymbolicLink() &&
|
|
312
|
+
strictRelativeRegularFile(path, binRel) &&
|
|
313
|
+
strictMarkerMatches(path, expectedSha);
|
|
94
314
|
} catch {
|
|
95
315
|
return false;
|
|
96
316
|
}
|
|
97
317
|
}
|
|
98
318
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
319
|
+
function hasInstallResidue(versionDir, base) {
|
|
320
|
+
return readdirSync(versionDir).some((name) =>
|
|
321
|
+
name.startsWith(`.${base}.install-`) ||
|
|
322
|
+
name.startsWith(`${base}.old-`));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function durabilityEvent(hook, event, details) {
|
|
326
|
+
hook?.(event, details);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function syncRegularFile(path, event, hook) {
|
|
330
|
+
durabilityEvent(hook, event, { path });
|
|
331
|
+
const before = lstatSync(path, { bigint: true });
|
|
332
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
333
|
+
throw new Error(`agenc: durability target is not a regular file: ${path}`);
|
|
334
|
+
}
|
|
335
|
+
const fd = openSync(
|
|
336
|
+
path,
|
|
337
|
+
(process.platform === "win32" ? fsConstants.O_RDWR : fsConstants.O_RDONLY) |
|
|
338
|
+
(fsConstants.O_NOFOLLOW ?? 0) |
|
|
339
|
+
(fsConstants.O_NONBLOCK ?? 0),
|
|
340
|
+
);
|
|
341
|
+
try {
|
|
342
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
343
|
+
if (!opened.isFile() || !sameLocalArtifactIdentity(before, opened)) {
|
|
344
|
+
throw new Error(`agenc: durability target changed while it was opened: ${path}`);
|
|
345
|
+
}
|
|
346
|
+
fsyncSync(fd);
|
|
347
|
+
} finally {
|
|
348
|
+
closeSync(fd);
|
|
349
|
+
}
|
|
103
350
|
}
|
|
104
351
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
352
|
+
function syncDirectory(path, event, hook) {
|
|
353
|
+
if (process.platform === "win32") {
|
|
354
|
+
// Node does not expose Win32 directory handles with write-through flags.
|
|
355
|
+
// Reopen and enumerate the renamed directory so Windows at least verifies
|
|
356
|
+
// the new name, while every regular file is flushed individually.
|
|
357
|
+
durabilityEvent(hook, `verify-${event}`, { path });
|
|
358
|
+
const metadata = lstatSync(path);
|
|
359
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
360
|
+
throw new Error(`agenc: durable directory verification failed: ${path}`);
|
|
361
|
+
}
|
|
362
|
+
readdirSync(path);
|
|
109
363
|
return;
|
|
110
364
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
365
|
+
durabilityEvent(hook, event, { path });
|
|
366
|
+
const fd = openSync(
|
|
367
|
+
path,
|
|
368
|
+
fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0),
|
|
369
|
+
);
|
|
370
|
+
try {
|
|
371
|
+
fsyncSync(fd);
|
|
372
|
+
} finally {
|
|
373
|
+
closeSync(fd);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function syncExtractedTree(root, hook) {
|
|
378
|
+
const entries = readdirSync(root, { withFileTypes: true })
|
|
379
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const path = join(root, entry.name);
|
|
382
|
+
const metadata = lstatSync(path);
|
|
383
|
+
if (metadata.isSymbolicLink()) continue;
|
|
384
|
+
if (metadata.isDirectory()) {
|
|
385
|
+
syncExtractedTree(path, hook);
|
|
386
|
+
} else if (metadata.isFile()) {
|
|
387
|
+
syncRegularFile(path, "sync-tree-file", hook);
|
|
388
|
+
} else {
|
|
389
|
+
throw new Error(`agenc: extracted runtime contains an unsupported file type: ${path}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
syncDirectory(root, "sync-tree-directory", hook);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function durableWriteMarker(path, content, hook) {
|
|
396
|
+
durabilityEvent(hook, "write-marker", { path });
|
|
397
|
+
const fd = openSync(
|
|
398
|
+
path,
|
|
399
|
+
fsConstants.O_WRONLY |
|
|
400
|
+
fsConstants.O_CREAT |
|
|
401
|
+
fsConstants.O_EXCL |
|
|
402
|
+
(fsConstants.O_NOFOLLOW ?? 0),
|
|
403
|
+
0o600,
|
|
404
|
+
);
|
|
405
|
+
try {
|
|
406
|
+
const bytes = Buffer.from(content);
|
|
407
|
+
let offset = 0;
|
|
408
|
+
while (offset < bytes.length) {
|
|
409
|
+
const written = writeSync(fd, bytes, offset, bytes.length - offset, null);
|
|
410
|
+
if (written <= 0) throw new Error("agenc: marker write made no progress");
|
|
411
|
+
offset += written;
|
|
412
|
+
}
|
|
413
|
+
durabilityEvent(hook, "sync-marker", { path });
|
|
414
|
+
fsyncSync(fd);
|
|
415
|
+
} finally {
|
|
416
|
+
closeSync(fd);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function verifyRenamedDirectory(path, hook) {
|
|
421
|
+
if (process.platform !== "win32") return;
|
|
422
|
+
durabilityEvent(hook, "verify-rename", { path });
|
|
423
|
+
const metadata = lstatSync(path);
|
|
424
|
+
if (
|
|
425
|
+
!metadata.isDirectory() ||
|
|
426
|
+
metadata.isSymbolicLink()
|
|
427
|
+
) {
|
|
428
|
+
throw new Error(`agenc: renamed runtime directory failed verification: ${path}`);
|
|
429
|
+
}
|
|
430
|
+
readdirSync(path);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function durableRenameDirectory(from, to, event, hook) {
|
|
434
|
+
durabilityEvent(hook, event, { from, to });
|
|
435
|
+
renameSync(from, to);
|
|
436
|
+
verifyRenamedDirectory(to, hook);
|
|
437
|
+
syncDirectory(dirname(to), "sync-parent-after-rename", hook);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function durableRemoveDirectory(path, parent, hook) {
|
|
441
|
+
durabilityEvent(hook, "remove-residue", { path });
|
|
442
|
+
rmSync(path, { recursive: true, force: true });
|
|
443
|
+
syncDirectory(parent, "sync-parent-after-remove", hook);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function reconcileInstallState(versionDir, installDir, expectedSha, binRel, durabilityHook) {
|
|
447
|
+
const base = basename(installDir);
|
|
448
|
+
const entries = readdirSync(versionDir);
|
|
449
|
+
const readyStages = entries
|
|
450
|
+
.filter((name) => name.startsWith(`.${base}.install-`))
|
|
451
|
+
.map((name) => join(versionDir, name))
|
|
452
|
+
.filter((path) => readyInstallAt(path, expectedSha, binRel))
|
|
453
|
+
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
|
|
454
|
+
const readyBackups = entries
|
|
455
|
+
.filter((name) => name.startsWith(`${base}.old-`))
|
|
456
|
+
.map((name) => join(versionDir, name))
|
|
457
|
+
.filter((path) => readyInstallAt(path, expectedSha, binRel))
|
|
458
|
+
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
|
|
459
|
+
|
|
460
|
+
if (!readyInstallAt(installDir, expectedSha, binRel)) {
|
|
461
|
+
// A staged tree is written and marked before the first promotion rename,
|
|
462
|
+
// so it is the newest intended state. Prefer it over a backup, then use a
|
|
463
|
+
// verified backup as the fallback. promoteInstall quarantines any invalid
|
|
464
|
+
// canonical tree and rolls it back if the candidate rename fails.
|
|
465
|
+
const recoveryCandidate = readyStages[0] ?? readyBackups[0];
|
|
466
|
+
if (recoveryCandidate !== undefined) {
|
|
467
|
+
syncExtractedTree(recoveryCandidate, durabilityHook);
|
|
468
|
+
promoteInstall(recoveryCandidate, installDir, durabilityHook);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const ready = readyInstallAt(installDir, expectedSha, binRel);
|
|
472
|
+
// Never destroy crash evidence or the only recoverable tree until a fully
|
|
473
|
+
// verified canonical installation exists.
|
|
474
|
+
if (!ready) return false;
|
|
475
|
+
for (const name of readdirSync(versionDir)) {
|
|
476
|
+
if (
|
|
477
|
+
name.startsWith(`.${base}.install-`) ||
|
|
478
|
+
name.startsWith(`${base}.old-`)
|
|
479
|
+
) {
|
|
480
|
+
try {
|
|
481
|
+
durableRemoveDirectory(join(versionDir, name), versionDir, durabilityHook);
|
|
482
|
+
} catch { /* residue cleanup is retried on the next launch */ }
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function promoteInstall(stagingDir, installDir, durabilityHook) {
|
|
489
|
+
const backup = `${installDir}.old-${process.pid}-${randomUUID()}`;
|
|
490
|
+
let movedExisting = false;
|
|
491
|
+
try {
|
|
492
|
+
if (existsSync(installDir)) {
|
|
493
|
+
durableRenameDirectory(
|
|
494
|
+
installDir,
|
|
495
|
+
backup,
|
|
496
|
+
"rename-current-to-backup",
|
|
497
|
+
durabilityHook,
|
|
498
|
+
);
|
|
499
|
+
movedExisting = true;
|
|
500
|
+
}
|
|
501
|
+
durableRenameDirectory(
|
|
502
|
+
stagingDir,
|
|
503
|
+
installDir,
|
|
504
|
+
"rename-stage-to-current",
|
|
505
|
+
durabilityHook,
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (!existsSync(installDir) && movedExisting && existsSync(backup)) {
|
|
509
|
+
try {
|
|
510
|
+
durableRenameDirectory(
|
|
511
|
+
backup,
|
|
512
|
+
installDir,
|
|
513
|
+
"rename-backup-to-current",
|
|
514
|
+
durabilityHook,
|
|
515
|
+
);
|
|
516
|
+
} catch (rollbackError) {
|
|
517
|
+
throw new AggregateError(
|
|
518
|
+
[error, rollbackError],
|
|
519
|
+
`agenc: runtime promotion failed; previous install retained at ${backup}`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function identityOf(stats) {
|
|
528
|
+
return {
|
|
529
|
+
dev: stats.dev,
|
|
530
|
+
ino: stats.ino,
|
|
531
|
+
size: stats.size,
|
|
532
|
+
mtimeNs: stats.mtimeNs,
|
|
533
|
+
ctimeNs: stats.ctimeNs,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function sameIdentity(left, right, { includeContent = true } = {}) {
|
|
538
|
+
return left.dev === right.dev && left.ino === right.ino &&
|
|
539
|
+
(!includeContent || (
|
|
540
|
+
left.size === right.size &&
|
|
541
|
+
left.mtimeNs === right.mtimeNs &&
|
|
542
|
+
left.ctimeNs === right.ctimeNs
|
|
543
|
+
));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function snapshotRegularFile(path, label) {
|
|
547
|
+
const stats = lstatSync(path, { bigint: true });
|
|
548
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1n) {
|
|
549
|
+
throw new Error(`agenc: ${label} must be a regular single-link file`);
|
|
550
|
+
}
|
|
551
|
+
return identityOf(stats);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function assertRegularFileIdentity(path, expected, label) {
|
|
555
|
+
const actual = snapshotRegularFile(path, label);
|
|
556
|
+
if (!sameIdentity(actual, expected)) {
|
|
557
|
+
throw new Error(`agenc: ${label} identity changed during installation`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function snapshotDirectory(path, label) {
|
|
562
|
+
const stats = lstatSync(path, { bigint: true });
|
|
563
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
564
|
+
throw new Error(`agenc: ${label} must be a real directory`);
|
|
565
|
+
}
|
|
566
|
+
return identityOf(stats);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function assertDirectoryIdentity(path, expected, label) {
|
|
570
|
+
const actual = snapshotDirectory(path, label);
|
|
571
|
+
if (!sameIdentity(actual, expected, { includeContent: false })) {
|
|
572
|
+
throw new Error(`agenc: ${label} identity changed during installation`);
|
|
114
573
|
}
|
|
115
|
-
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function createPrivateTemporaryDirectory(parent, prefix) {
|
|
577
|
+
const requestedParent = resolve(parent);
|
|
578
|
+
const canonicalParent = realpathSync.native(requestedParent);
|
|
579
|
+
if (canonicalParent !== requestedParent) {
|
|
580
|
+
throw new Error(`agenc: temporary parent must use its canonical path: ${requestedParent}`);
|
|
581
|
+
}
|
|
582
|
+
const parentStats = lstatSync(canonicalParent, { bigint: true });
|
|
583
|
+
if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
|
|
584
|
+
throw new Error(`agenc: temporary parent is not a real directory: ${canonicalParent}`);
|
|
585
|
+
}
|
|
586
|
+
if (process.platform !== "win32" && typeof process.getuid === "function") {
|
|
587
|
+
if (parentStats.uid !== BigInt(process.getuid()) || (parentStats.mode & 0o022n) !== 0n) {
|
|
588
|
+
throw new Error(`agenc: temporary parent is not privately owned: ${canonicalParent}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const created = mkdtempSync(join(canonicalParent, prefix));
|
|
592
|
+
const before = snapshotDirectory(created, "temporary directory");
|
|
593
|
+
chmodSync(created, 0o700);
|
|
594
|
+
const canonical = realpathSync.native(created);
|
|
595
|
+
const after = snapshotDirectory(canonical, "temporary directory");
|
|
596
|
+
if (canonical !== created || !sameIdentity(before, after, { includeContent: false })) {
|
|
597
|
+
throw new Error("agenc: temporary directory identity changed while it was secured");
|
|
598
|
+
}
|
|
599
|
+
return { path: canonical, identity: after };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function sha256File(file, expectedIdentity) {
|
|
603
|
+
const before = snapshotRegularFile(file, "runtime archive");
|
|
604
|
+
if (expectedIdentity !== undefined && !sameIdentity(before, expectedIdentity)) {
|
|
605
|
+
throw new Error("agenc: runtime archive identity changed before hashing");
|
|
606
|
+
}
|
|
607
|
+
const fd = openSync(
|
|
608
|
+
file,
|
|
609
|
+
fsConstants.O_RDONLY |
|
|
610
|
+
(fsConstants.O_NOFOLLOW ?? 0) |
|
|
611
|
+
(fsConstants.O_NONBLOCK ?? 0),
|
|
612
|
+
);
|
|
613
|
+
try {
|
|
614
|
+
const opened = identityOf(fstatSync(fd, { bigint: true }));
|
|
615
|
+
if (!sameIdentity(opened, before)) {
|
|
616
|
+
throw new Error("agenc: runtime archive changed while it was opened for hashing");
|
|
617
|
+
}
|
|
618
|
+
const hash = createHash("sha256");
|
|
619
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
620
|
+
for (;;) {
|
|
621
|
+
const count = readSync(fd, buffer, 0, buffer.length, null);
|
|
622
|
+
if (count === 0) break;
|
|
623
|
+
hash.update(buffer.subarray(0, count));
|
|
624
|
+
}
|
|
625
|
+
const after = identityOf(fstatSync(fd, { bigint: true }));
|
|
626
|
+
if (!sameIdentity(after, before)) {
|
|
627
|
+
throw new Error("agenc: runtime archive changed while it was hashed");
|
|
628
|
+
}
|
|
629
|
+
assertRegularFileIdentity(file, before, "runtime archive");
|
|
630
|
+
return hash.digest("hex");
|
|
631
|
+
} finally {
|
|
632
|
+
closeSync(fd);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
637
|
+
|
|
638
|
+
function createFetchDeadline(timeoutMs) {
|
|
639
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 600_000) {
|
|
640
|
+
throw new TypeError("agenc: runtime fetch timeout must be an integer from 1 to 600000 ms");
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
controller: new AbortController(),
|
|
644
|
+
deadline: performance.now() + timeoutMs,
|
|
645
|
+
timeoutMs,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function fetchTimeoutError(context, label) {
|
|
650
|
+
const error = new Error(
|
|
651
|
+
`agenc: ${label} timed out after ${context.timeoutMs} ms total`,
|
|
652
|
+
);
|
|
653
|
+
error.code = "AGENC_RUNTIME_FETCH_TIMEOUT";
|
|
654
|
+
return error;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function awaitWithinFetchDeadline(value, context, label) {
|
|
658
|
+
const remaining = context.deadline - performance.now();
|
|
659
|
+
if (remaining <= 0) {
|
|
660
|
+
const error = fetchTimeoutError(context, label);
|
|
661
|
+
context.controller.abort(error);
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
let timer;
|
|
665
|
+
const expired = new Promise((_, reject) => {
|
|
666
|
+
timer = setTimeout(() => {
|
|
667
|
+
const error = fetchTimeoutError(context, label);
|
|
668
|
+
// Settle the deadline promise with the policy error before aborting the
|
|
669
|
+
// transport/pipeline, whose AbortError would otherwise obscure it.
|
|
670
|
+
reject(error);
|
|
671
|
+
context.controller.abort(error);
|
|
672
|
+
}, Math.max(1, Math.ceil(remaining)));
|
|
673
|
+
});
|
|
674
|
+
try {
|
|
675
|
+
return await Promise.race([Promise.resolve(value), expired]);
|
|
676
|
+
} finally {
|
|
677
|
+
clearTimeout(timer);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function cancelResponseBody(response, context, label) {
|
|
682
|
+
if (typeof response?.body?.cancel !== "function") return;
|
|
683
|
+
try {
|
|
684
|
+
const cancellation = response.body.cancel();
|
|
685
|
+
if (context === undefined) {
|
|
686
|
+
await cancellation;
|
|
687
|
+
} else {
|
|
688
|
+
await awaitWithinFetchDeadline(cancellation, context, label);
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
if (error?.code === "AGENC_RUNTIME_FETCH_TIMEOUT") throw error;
|
|
692
|
+
// Cancellation is best-effort after a stronger policy error. Aborting the
|
|
693
|
+
// shared controller below still tears down the transport.
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function fetchHttps(url, fetchImpl, label, context) {
|
|
698
|
+
let current;
|
|
699
|
+
try {
|
|
700
|
+
current = new URL(url);
|
|
701
|
+
} catch {
|
|
702
|
+
throw new Error(`agenc: ${label} URL is invalid`);
|
|
703
|
+
}
|
|
704
|
+
if (current.protocol !== "https:") {
|
|
705
|
+
throw new Error(`agenc: refusing non-HTTPS ${label} URL: ${current.href}`);
|
|
706
|
+
}
|
|
707
|
+
for (let redirects = 0; redirects <= 5; redirects += 1) {
|
|
708
|
+
const response = await awaitWithinFetchDeadline(
|
|
709
|
+
fetchImpl(current, {
|
|
710
|
+
redirect: "manual",
|
|
711
|
+
headers: { "accept-encoding": "identity" },
|
|
712
|
+
signal: context.controller.signal,
|
|
713
|
+
}),
|
|
714
|
+
context,
|
|
715
|
+
`${label} response headers`,
|
|
716
|
+
);
|
|
717
|
+
if (!REDIRECT_STATUSES.has(response.status)) {
|
|
718
|
+
let finalProtocolIsHttps = true;
|
|
719
|
+
try {
|
|
720
|
+
finalProtocolIsHttps = !response.url || new URL(response.url).protocol === "https:";
|
|
721
|
+
} catch {
|
|
722
|
+
finalProtocolIsHttps = false;
|
|
723
|
+
}
|
|
724
|
+
if (response.redirected || !finalProtocolIsHttps) {
|
|
725
|
+
await cancelResponseBody(response, context, `${label} response cancellation`);
|
|
726
|
+
throw new Error(`agenc: ${label} fetch performed an unreviewed redirect`);
|
|
727
|
+
}
|
|
728
|
+
return response;
|
|
729
|
+
}
|
|
730
|
+
const location = response.headers?.get?.("location");
|
|
731
|
+
if (!location) {
|
|
732
|
+
await cancelResponseBody(response, context, `${label} redirect cancellation`);
|
|
733
|
+
throw new Error(`agenc: ${label} redirect is missing Location`);
|
|
734
|
+
}
|
|
735
|
+
let next;
|
|
736
|
+
try {
|
|
737
|
+
next = new URL(location, current);
|
|
738
|
+
} catch {
|
|
739
|
+
await cancelResponseBody(response, context, `${label} redirect cancellation`);
|
|
740
|
+
throw new Error(`agenc: ${label} redirect Location is invalid`);
|
|
741
|
+
}
|
|
742
|
+
if (next.protocol !== "https:") {
|
|
743
|
+
await cancelResponseBody(response, context, `${label} redirect cancellation`);
|
|
744
|
+
throw new Error(`agenc: refusing HTTPS downgrade while fetching ${label}`);
|
|
745
|
+
}
|
|
746
|
+
await cancelResponseBody(response, context, `${label} redirect cancellation`);
|
|
747
|
+
current = next;
|
|
748
|
+
}
|
|
749
|
+
throw new Error(`agenc: too many redirects while fetching ${label}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function* requireExactByteCount(source, expectedBytes, context) {
|
|
753
|
+
let received = 0;
|
|
754
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
755
|
+
let complete = false;
|
|
756
|
+
try {
|
|
757
|
+
while (true) {
|
|
758
|
+
const step = context === undefined
|
|
759
|
+
? await iterator.next()
|
|
760
|
+
: await awaitWithinFetchDeadline(
|
|
761
|
+
iterator.next(),
|
|
762
|
+
context,
|
|
763
|
+
"runtime artifact response body",
|
|
764
|
+
);
|
|
765
|
+
if (step.done) {
|
|
766
|
+
complete = true;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
const chunk = Buffer.isBuffer(step.value) ? step.value : Buffer.from(step.value);
|
|
770
|
+
received += chunk.length;
|
|
771
|
+
if (received > expectedBytes) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`agenc: runtime byte count exceeds signed size ` +
|
|
774
|
+
`(expected ${expectedBytes}, received at least ${received})`,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
yield chunk;
|
|
778
|
+
}
|
|
779
|
+
} finally {
|
|
780
|
+
if (!complete && typeof iterator.return === "function") {
|
|
781
|
+
try {
|
|
782
|
+
const returning = iterator.return();
|
|
783
|
+
if (context === undefined) await returning;
|
|
784
|
+
else await awaitWithinFetchDeadline(
|
|
785
|
+
returning,
|
|
786
|
+
context,
|
|
787
|
+
"runtime artifact body cancellation",
|
|
788
|
+
);
|
|
789
|
+
} catch { /* the primary error wins */ }
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (received !== expectedBytes) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`agenc: runtime byte count mismatch (expected ${expectedBytes}, got ${received})`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function parseContentLength(response) {
|
|
800
|
+
const value = response.headers?.get?.("content-length");
|
|
801
|
+
if (value === null || value === undefined) return undefined;
|
|
802
|
+
if (!/^(0|[1-9][0-9]*)$/.test(value)) {
|
|
803
|
+
throw new Error("agenc: runtime artifact Content-Length is invalid");
|
|
804
|
+
}
|
|
805
|
+
const parsed = Number(value);
|
|
806
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
807
|
+
throw new Error("agenc: runtime artifact Content-Length is invalid");
|
|
808
|
+
}
|
|
809
|
+
return parsed;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function sameLocalArtifactIdentity(left, right) {
|
|
813
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function sameLocalArtifactSnapshot(left, right) {
|
|
817
|
+
return sameLocalArtifactIdentity(left, right) &&
|
|
818
|
+
left.nlink === right.nlink &&
|
|
819
|
+
left.size === right.size &&
|
|
820
|
+
left.mode === right.mode &&
|
|
821
|
+
left.mtimeNs === right.mtimeNs &&
|
|
822
|
+
left.ctimeNs === right.ctimeNs;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function assertPlainSingleLinkArtifact(metadata, expectedBytes) {
|
|
826
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n) {
|
|
827
|
+
throw new Error("agenc: local runtime artifact must be a plain single-link file");
|
|
828
|
+
}
|
|
829
|
+
if (metadata.dev === 0n && metadata.ino === 0n) {
|
|
830
|
+
throw new Error("agenc: local runtime artifact has no stable filesystem identity");
|
|
831
|
+
}
|
|
832
|
+
if (metadata.size !== BigInt(expectedBytes)) {
|
|
833
|
+
throw new Error(
|
|
834
|
+
`agenc: runtime byte count mismatch (expected ${expectedBytes}, got ${metadata.size})`,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function copyLocalArtifact(
|
|
840
|
+
url,
|
|
841
|
+
dest,
|
|
842
|
+
expectedBytes,
|
|
843
|
+
localArtifactHook,
|
|
844
|
+
durabilityHook,
|
|
845
|
+
) {
|
|
846
|
+
const path = canonicalLocalFileUrlToPath(url);
|
|
847
|
+
const before = lstatSync(path, { bigint: true });
|
|
848
|
+
assertPlainSingleLinkArtifact(before, expectedBytes);
|
|
849
|
+
localArtifactHook?.("after-lstat", { path, metadata: before });
|
|
850
|
+
const flags = fsConstants.O_RDONLY |
|
|
851
|
+
(fsConstants.O_NOFOLLOW ?? 0) |
|
|
852
|
+
(fsConstants.O_NONBLOCK ?? 0);
|
|
853
|
+
let fd;
|
|
854
|
+
let operationError;
|
|
855
|
+
try {
|
|
856
|
+
fd = openSync(path, flags);
|
|
857
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
858
|
+
assertPlainSingleLinkArtifact(opened, expectedBytes);
|
|
859
|
+
if (!sameLocalArtifactSnapshot(before, opened)) {
|
|
860
|
+
throw new Error("agenc: local runtime artifact changed while it was opened");
|
|
861
|
+
}
|
|
862
|
+
localArtifactHook?.("after-open", { path, fd, metadata: opened });
|
|
863
|
+
await pipeline(
|
|
864
|
+
createReadStream(path, { fd, autoClose: false }),
|
|
865
|
+
(source) => requireExactByteCount(source, expectedBytes),
|
|
866
|
+
createWriteStream(dest, { flags: "wx", mode: 0o600 }),
|
|
867
|
+
);
|
|
868
|
+
localArtifactHook?.("before-eof-validation", { path, fd, metadata: opened });
|
|
869
|
+
const openedAfterRead = fstatSync(fd, { bigint: true });
|
|
870
|
+
const pathAfterRead = lstatSync(path, { bigint: true });
|
|
871
|
+
assertPlainSingleLinkArtifact(openedAfterRead, expectedBytes);
|
|
872
|
+
assertPlainSingleLinkArtifact(pathAfterRead, expectedBytes);
|
|
873
|
+
if (
|
|
874
|
+
!sameLocalArtifactSnapshot(opened, openedAfterRead) ||
|
|
875
|
+
!sameLocalArtifactSnapshot(openedAfterRead, pathAfterRead)
|
|
876
|
+
) {
|
|
877
|
+
throw new Error("agenc: local runtime artifact changed while it was read");
|
|
878
|
+
}
|
|
879
|
+
syncRegularFile(dest, "sync-download", durabilityHook);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
operationError = error;
|
|
882
|
+
}
|
|
883
|
+
let closeError;
|
|
884
|
+
if (fd !== undefined) {
|
|
885
|
+
try { closeSync(fd); } catch (error) { closeError = error; }
|
|
886
|
+
}
|
|
887
|
+
if (operationError !== undefined && closeError !== undefined) {
|
|
888
|
+
throw new AggregateError(
|
|
889
|
+
[operationError, closeError],
|
|
890
|
+
"agenc: local runtime artifact copy and descriptor close did not both complete",
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
if (operationError !== undefined) throw operationError;
|
|
894
|
+
if (closeError !== undefined) throw closeError;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async function download(
|
|
898
|
+
url,
|
|
899
|
+
dest,
|
|
900
|
+
expectedBytes,
|
|
901
|
+
trustMode,
|
|
902
|
+
fetchImpl = globalThis.fetch,
|
|
903
|
+
{
|
|
904
|
+
fetchTimeoutMs = DEFAULT_RUNTIME_FETCH_TIMEOUT_MS,
|
|
905
|
+
localArtifactHook,
|
|
906
|
+
durabilityHook,
|
|
907
|
+
} = {},
|
|
908
|
+
) {
|
|
909
|
+
if (
|
|
910
|
+
!Number.isSafeInteger(expectedBytes) ||
|
|
911
|
+
expectedBytes <= 0 ||
|
|
912
|
+
expectedBytes > MAX_RUNTIME_ARTIFACT_BYTES
|
|
913
|
+
) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
`agenc: runtime artifact signed size must be between 1 and ` +
|
|
916
|
+
`${MAX_RUNTIME_ARTIFACT_BYTES} bytes`,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (trustMode === "explicitLocal") {
|
|
920
|
+
await copyLocalArtifact(url, dest, expectedBytes, localArtifactHook, durabilityHook);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const parsed = new URL(url);
|
|
924
|
+
if (parsed.protocol === "file:") {
|
|
925
|
+
throw new Error("agenc: remote runtime manifests must not access local files");
|
|
926
|
+
}
|
|
927
|
+
const context = createFetchDeadline(fetchTimeoutMs);
|
|
928
|
+
let res;
|
|
929
|
+
try {
|
|
930
|
+
res = await fetchHttps(url, fetchImpl, "runtime artifact", context);
|
|
931
|
+
if (!res.ok || res.body === null) {
|
|
932
|
+
await cancelResponseBody(res, context, "runtime artifact error-body cancellation");
|
|
933
|
+
throw new Error(`agenc: download failed ${res.status} ${res.statusText}: ${url}`);
|
|
934
|
+
}
|
|
935
|
+
const contentEncoding = res.headers?.get?.("content-encoding");
|
|
936
|
+
if (
|
|
937
|
+
contentEncoding !== null &&
|
|
938
|
+
contentEncoding !== undefined &&
|
|
939
|
+
contentEncoding !== "identity"
|
|
940
|
+
) {
|
|
941
|
+
await cancelResponseBody(res, context, "runtime artifact error-body cancellation");
|
|
942
|
+
throw new Error("agenc: runtime artifact response must use identity encoding");
|
|
943
|
+
}
|
|
944
|
+
let contentLength;
|
|
945
|
+
try {
|
|
946
|
+
contentLength = parseContentLength(res);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
await cancelResponseBody(res, context, "runtime artifact error-body cancellation");
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
if (contentLength !== undefined && contentLength !== expectedBytes) {
|
|
952
|
+
await cancelResponseBody(res, context, "runtime artifact error-body cancellation");
|
|
953
|
+
throw new Error(
|
|
954
|
+
`agenc: runtime artifact Content-Length mismatch ` +
|
|
955
|
+
`(expected ${expectedBytes}, got ${contentLength})`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const readable = typeof res.body.getReader === "function"
|
|
959
|
+
? Readable.fromWeb(res.body)
|
|
960
|
+
: res.body;
|
|
961
|
+
await awaitWithinFetchDeadline(
|
|
962
|
+
pipeline(
|
|
963
|
+
readable,
|
|
964
|
+
(source) => requireExactByteCount(source, expectedBytes, context),
|
|
965
|
+
createWriteStream(dest, { flags: "wx", mode: 0o600 }),
|
|
966
|
+
{ signal: context.controller.signal },
|
|
967
|
+
),
|
|
968
|
+
context,
|
|
969
|
+
"runtime artifact download",
|
|
970
|
+
);
|
|
971
|
+
syncRegularFile(dest, "sync-download", durabilityHook);
|
|
972
|
+
} catch (error) {
|
|
973
|
+
context.controller.abort(error);
|
|
974
|
+
if (res !== undefined) {
|
|
975
|
+
// Do not let a transport that ignores abort hold the installer open.
|
|
976
|
+
void cancelResponseBody(res, undefined, "runtime artifact cancellation")
|
|
977
|
+
.catch(() => {});
|
|
978
|
+
}
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function assertRootOwnedSystemExecutable(path) {
|
|
984
|
+
const canonical = realpathSync.native(path);
|
|
985
|
+
const executable = lstatSync(canonical, { bigint: true });
|
|
986
|
+
if (
|
|
987
|
+
!executable.isFile() || executable.isSymbolicLink() ||
|
|
988
|
+
executable.nlink !== 1n || executable.uid !== 0n ||
|
|
989
|
+
(executable.mode & 0o022n) !== 0n
|
|
990
|
+
) {
|
|
991
|
+
throw new Error(`agenc: operating-system tar is not a trusted regular file: ${canonical}`);
|
|
992
|
+
}
|
|
993
|
+
for (let ancestor = dirname(canonical); ; ancestor = dirname(ancestor)) {
|
|
994
|
+
const metadata = lstatSync(ancestor, { bigint: true });
|
|
995
|
+
if (
|
|
996
|
+
!metadata.isDirectory() || metadata.isSymbolicLink() ||
|
|
997
|
+
metadata.uid !== 0n || (metadata.mode & 0o022n) !== 0n
|
|
998
|
+
) {
|
|
999
|
+
throw new Error(`agenc: operating-system tar has an untrusted ancestor: ${ancestor}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (dirname(ancestor) === ancestor) break;
|
|
1002
|
+
}
|
|
1003
|
+
return canonical;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Resolve tar independently of npm/project PATH and caller environment controls. */
|
|
1007
|
+
export function resolveTrustedSystemTar(platform = process.platform) {
|
|
1008
|
+
if (platform === "linux" || platform === "darwin") {
|
|
1009
|
+
const candidates = platform === "darwin"
|
|
1010
|
+
? ["/usr/bin/tar"]
|
|
1011
|
+
: ["/usr/bin/tar", "/bin/tar"];
|
|
1012
|
+
const candidate = candidates.find((path) => existsSync(path));
|
|
1013
|
+
if (candidate === undefined) {
|
|
1014
|
+
throw new Error(`agenc: trusted operating-system tar is unavailable on ${platform}`);
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
path: assertRootOwnedSystemExecutable(candidate),
|
|
1018
|
+
env: { PATH: "/usr/bin:/bin", LANG: "C", LC_ALL: "C", TZ: "UTC" },
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
if (platform !== "win32") {
|
|
1022
|
+
throw new Error(`agenc: trusted operating-system tar is unsupported on ${platform}`);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// GLOBALROOT reaches the kernel's real SystemRoot namespace and cannot be
|
|
1026
|
+
// redirected with caller-controlled SystemRoot/WINDIR values or drive maps.
|
|
1027
|
+
const system32 = win32.join(WINDOWS_SYSTEM_ROOT, "System32");
|
|
1028
|
+
const powershellRoot = win32.join(system32, "WindowsPowerShell", "v1.0");
|
|
1029
|
+
const tarPath = win32.join(system32, "tar.exe");
|
|
1030
|
+
const powershell = win32.join(powershellRoot, "powershell.exe");
|
|
1031
|
+
for (const [path, label] of [[tarPath, "tar"], [powershell, "PowerShell"]]) {
|
|
1032
|
+
const metadata = lstatSync(path, { bigint: true });
|
|
1033
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
1034
|
+
throw new Error(`agenc: trusted Windows ${label} is unavailable`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const env = {
|
|
1038
|
+
APPDATA: "",
|
|
1039
|
+
COMSPEC: win32.join(system32, "cmd.exe"),
|
|
1040
|
+
HOME: "",
|
|
1041
|
+
LOCALAPPDATA: "",
|
|
1042
|
+
PATH: `${system32};${powershellRoot}`,
|
|
1043
|
+
PATHEXT: ".COM;.EXE",
|
|
1044
|
+
PSModulePath: win32.join(powershellRoot, "Modules"),
|
|
1045
|
+
SystemRoot: WINDOWS_SYSTEM_ROOT,
|
|
1046
|
+
TEMP: win32.join(WINDOWS_SYSTEM_ROOT, "Temp"),
|
|
1047
|
+
TMP: win32.join(WINDOWS_SYSTEM_ROOT, "Temp"),
|
|
1048
|
+
USERPROFILE: powershellRoot,
|
|
1049
|
+
WINDIR: WINDOWS_SYSTEM_ROOT,
|
|
1050
|
+
};
|
|
1051
|
+
const signature = spawnSync(
|
|
1052
|
+
powershell,
|
|
1053
|
+
[
|
|
1054
|
+
"-NoLogo",
|
|
1055
|
+
"-NoProfile",
|
|
1056
|
+
"-NonInteractive",
|
|
1057
|
+
"-ExecutionPolicy",
|
|
1058
|
+
"Bypass",
|
|
1059
|
+
"-Command",
|
|
1060
|
+
"$s=Microsoft.PowerShell.Security\\Get-AuthenticodeSignature -LiteralPath $args[0];" +
|
|
1061
|
+
"if($s.Status -ne 'Valid' -or $s.SignerCertificate.Subject -notmatch 'Microsoft'){exit 51}",
|
|
1062
|
+
tarPath,
|
|
1063
|
+
],
|
|
1064
|
+
{
|
|
1065
|
+
cwd: powershellRoot,
|
|
1066
|
+
encoding: "utf8",
|
|
1067
|
+
env,
|
|
1068
|
+
maxBuffer: 1024 * 1024,
|
|
1069
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1070
|
+
timeout: 30_000,
|
|
1071
|
+
killSignal: "SIGKILL",
|
|
1072
|
+
windowsHide: true,
|
|
1073
|
+
},
|
|
1074
|
+
);
|
|
1075
|
+
if (signature.error !== undefined || signature.status !== 0) {
|
|
1076
|
+
throw new Error("agenc: Windows operating-system tar failed Authenticode validation");
|
|
1077
|
+
}
|
|
1078
|
+
return { path: tarPath, env };
|
|
116
1079
|
}
|
|
117
1080
|
|
|
118
1081
|
function extractTarGz(archive, destDir) {
|
|
119
1082
|
mkdirSync(destDir, { recursive: true, mode: 0o700 });
|
|
120
1083
|
try { chmodSync(destDir, 0o700); } catch { /* ignore */ }
|
|
121
|
-
const
|
|
122
|
-
|
|
1084
|
+
const tar = resolveTrustedSystemTar();
|
|
1085
|
+
const res = spawnSync(tar.path, ["-xzf", archive, "-C", destDir], {
|
|
1086
|
+
cwd: destDir,
|
|
1087
|
+
encoding: "utf8",
|
|
1088
|
+
env: tar.env,
|
|
1089
|
+
maxBuffer: 1024 * 1024,
|
|
1090
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1091
|
+
timeout: DEFAULT_RUNTIME_EXTRACTION_TIMEOUT_MS,
|
|
1092
|
+
killSignal: "SIGKILL",
|
|
1093
|
+
windowsHide: true,
|
|
123
1094
|
});
|
|
1095
|
+
if (res.error !== undefined) {
|
|
1096
|
+
throw new Error(`agenc: tar extraction failed: ${res.error.message}`, { cause: res.error });
|
|
1097
|
+
}
|
|
124
1098
|
if (res.status !== 0) {
|
|
125
|
-
|
|
1099
|
+
const detail = res.stderr?.trim();
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`agenc: tar extraction failed (status ${res.status ?? res.signal})` +
|
|
1102
|
+
(detail ? `: ${detail}` : ""),
|
|
1103
|
+
);
|
|
126
1104
|
}
|
|
127
1105
|
}
|
|
128
1106
|
|
|
@@ -134,45 +1112,169 @@ export async function ensureRuntime({
|
|
|
134
1112
|
manifest = readManifest(),
|
|
135
1113
|
slug = platformSlug(),
|
|
136
1114
|
fetchImpl = globalThis.fetch,
|
|
1115
|
+
runtimeFetchTimeoutMs = DEFAULT_RUNTIME_FETCH_TIMEOUT_MS,
|
|
1116
|
+
manifestTrust = "official",
|
|
1117
|
+
runtimeCompatibility,
|
|
1118
|
+
localArtifactHook,
|
|
1119
|
+
durabilityHook,
|
|
1120
|
+
acquireLock = acquireLocalSqliteLock,
|
|
1121
|
+
remove = rmSync,
|
|
137
1122
|
log = (m) => process.stderr.write(m + "\n"),
|
|
138
1123
|
} = {}) {
|
|
139
|
-
const
|
|
1124
|
+
const version = manifest.runtimeVersion;
|
|
1125
|
+
const artifact = selectArtifact(
|
|
1126
|
+
manifest,
|
|
1127
|
+
slug,
|
|
1128
|
+
process.versions.modules,
|
|
1129
|
+
runtimeCompatibility ?? currentRuntimeCompatibility(),
|
|
1130
|
+
{ trustMode: manifestTrust },
|
|
1131
|
+
);
|
|
1132
|
+
// Validate identity, provenance, compatibility, and byte ceilings before
|
|
1133
|
+
// creating or changing any installation path.
|
|
1134
|
+
const home = canonicalizeAgenCHome(resolveAgenCHome(env, userHome));
|
|
140
1135
|
// Owner-only home (todo-120); install.sh already chmods 700.
|
|
141
|
-
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
142
1136
|
try {
|
|
143
1137
|
chmodSync(home, 0o700);
|
|
144
1138
|
} catch {
|
|
145
1139
|
/* ignore */
|
|
146
1140
|
}
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
1141
|
+
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";
|
|
1144
|
+
const versionDir = dirname(installDir);
|
|
1145
|
+
mkdirSync(versionDir, { recursive: true, mode: 0o700 });
|
|
1146
|
+
chmodSync(versionDir, 0o700);
|
|
1147
|
+
const lockPath = `${installDir}.agenc-lock.sqlite`;
|
|
150
1148
|
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
1149
|
+
if (
|
|
1150
|
+
readyInstallAt(installDir, artifact.sha256, binRel) &&
|
|
1151
|
+
!hasInstallResidue(versionDir, basename(installDir))
|
|
1152
|
+
) return bin;
|
|
154
1153
|
|
|
155
|
-
|
|
156
|
-
|
|
1154
|
+
// Reconcile a prior crash before doing network I/O. A process killed between
|
|
1155
|
+
// the two promotion renames leaves a complete backup that can be restored
|
|
1156
|
+
// offline under the same artifact-scoped lock.
|
|
1157
|
+
let releaseLock;
|
|
1158
|
+
let downloadDir;
|
|
1159
|
+
let downloadDirIdentity;
|
|
1160
|
+
let stagingDir;
|
|
1161
|
+
let stagingDirIdentity;
|
|
1162
|
+
let operationError;
|
|
157
1163
|
try {
|
|
158
|
-
await
|
|
159
|
-
|
|
1164
|
+
releaseLock = await acquireLock(lockPath, {
|
|
1165
|
+
label: "runtime install",
|
|
1166
|
+
timeoutMs: 60_000,
|
|
1167
|
+
});
|
|
1168
|
+
if (reconcileInstallState(
|
|
1169
|
+
versionDir,
|
|
1170
|
+
installDir,
|
|
1171
|
+
artifact.sha256,
|
|
1172
|
+
binRel,
|
|
1173
|
+
durabilityHook,
|
|
1174
|
+
)) {
|
|
1175
|
+
return bin;
|
|
1176
|
+
}
|
|
1177
|
+
// Network I/O must not hold the filesystem lock. Keep the release handle
|
|
1178
|
+
// registered until it succeeds so the outer cleanup can retry a failed
|
|
1179
|
+
// SQLite close without losing the original error.
|
|
1180
|
+
releaseLock();
|
|
1181
|
+
releaseLock = undefined;
|
|
1182
|
+
|
|
1183
|
+
log(`agenc: fetching runtime ${version} (${slug.os}-${slug.arch})...`);
|
|
1184
|
+
({ path: downloadDir, identity: downloadDirIdentity } =
|
|
1185
|
+
createPrivateTemporaryDirectory(versionDir, ".agenc-runtime-download-"));
|
|
1186
|
+
const tmp = join(downloadDir, "runtime.tar.gz");
|
|
1187
|
+
await download(artifact.url, tmp, artifact.bytes, manifestTrust, fetchImpl, {
|
|
1188
|
+
fetchTimeoutMs: runtimeFetchTimeoutMs,
|
|
1189
|
+
localArtifactHook,
|
|
1190
|
+
durabilityHook,
|
|
1191
|
+
});
|
|
1192
|
+
const archiveIdentity = snapshotRegularFile(tmp, "runtime archive");
|
|
1193
|
+
const actual = sha256File(tmp, archiveIdentity);
|
|
160
1194
|
if (actual !== artifact.sha256) {
|
|
161
1195
|
throw new Error(
|
|
162
1196
|
`agenc: runtime checksum mismatch (expected ${artifact.sha256}, got ${actual})`,
|
|
163
1197
|
);
|
|
164
1198
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
1199
|
+
if (archiveIdentity.size !== BigInt(artifact.bytes)) {
|
|
1200
|
+
throw new Error("agenc: bounded runtime download violated its byte-count invariant");
|
|
1201
|
+
}
|
|
1202
|
+
validateRuntimeArchive(tmp, artifact.platform);
|
|
1203
|
+
assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
|
|
1204
|
+
releaseLock = await acquireLock(lockPath, {
|
|
1205
|
+
label: "runtime install",
|
|
1206
|
+
timeoutMs: 60_000,
|
|
1207
|
+
});
|
|
1208
|
+
// Another process may have completed while this one downloaded.
|
|
1209
|
+
if (reconcileInstallState(
|
|
1210
|
+
versionDir,
|
|
1211
|
+
installDir,
|
|
1212
|
+
artifact.sha256,
|
|
1213
|
+
binRel,
|
|
1214
|
+
durabilityHook,
|
|
1215
|
+
)) return bin;
|
|
1216
|
+
({ path: stagingDir, identity: stagingDirIdentity } =
|
|
1217
|
+
createPrivateTemporaryDirectory(
|
|
1218
|
+
versionDir,
|
|
1219
|
+
`.${basename(installDir)}.install-`,
|
|
1220
|
+
));
|
|
1221
|
+
assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
|
|
1222
|
+
extractTarGz(tmp, stagingDir);
|
|
1223
|
+
assertRegularFileIdentity(tmp, archiveIdentity, "runtime archive");
|
|
1224
|
+
const stagedBin = join(stagingDir, binRel);
|
|
1225
|
+
if (!existsSync(stagedBin)) {
|
|
1226
|
+
throw new Error(`agenc: runtime extracted but entry missing: ${stagedBin}`);
|
|
1227
|
+
}
|
|
1228
|
+
durableWriteMarker(markerPath(stagingDir), artifact.sha256, durabilityHook);
|
|
1229
|
+
syncExtractedTree(stagingDir, durabilityHook);
|
|
1230
|
+
promoteInstall(stagingDir, installDir, durabilityHook);
|
|
1231
|
+
stagingDir = undefined;
|
|
1232
|
+
stagingDirIdentity = undefined;
|
|
1233
|
+
if (!reconcileInstallState(
|
|
1234
|
+
versionDir,
|
|
1235
|
+
installDir,
|
|
1236
|
+
artifact.sha256,
|
|
1237
|
+
binRel,
|
|
1238
|
+
durabilityHook,
|
|
1239
|
+
)) {
|
|
1240
|
+
throw new Error("agenc: promoted runtime did not satisfy the marker contract");
|
|
171
1241
|
}
|
|
172
|
-
writeFileSync(markerPath(installDir), artifact.sha256);
|
|
173
1242
|
log(`agenc: runtime ${version} ready`);
|
|
174
1243
|
return bin;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
operationError = error;
|
|
1246
|
+
throw error;
|
|
175
1247
|
} finally {
|
|
176
|
-
|
|
1248
|
+
const cleanupErrors = [];
|
|
1249
|
+
if (stagingDir !== undefined && existsSync(stagingDir)) {
|
|
1250
|
+
try {
|
|
1251
|
+
assertDirectoryIdentity(stagingDir, stagingDirIdentity, "runtime staging directory");
|
|
1252
|
+
durabilityEvent(durabilityHook, "remove-temporary", { path: stagingDir });
|
|
1253
|
+
remove(stagingDir, { recursive: true, force: true });
|
|
1254
|
+
syncDirectory(dirname(stagingDir), "sync-parent-after-remove", durabilityHook);
|
|
1255
|
+
}
|
|
1256
|
+
catch (error) { cleanupErrors.push(error); }
|
|
1257
|
+
}
|
|
1258
|
+
if (releaseLock !== undefined) {
|
|
1259
|
+
try { releaseLock(); }
|
|
1260
|
+
catch (error) { cleanupErrors.push(error); }
|
|
1261
|
+
}
|
|
1262
|
+
if (downloadDir !== undefined) {
|
|
1263
|
+
try {
|
|
1264
|
+
assertDirectoryIdentity(downloadDir, downloadDirIdentity, "runtime download directory");
|
|
1265
|
+
durabilityEvent(durabilityHook, "remove-temporary", { path: downloadDir });
|
|
1266
|
+
remove(downloadDir, { recursive: true, force: true });
|
|
1267
|
+
syncDirectory(dirname(downloadDir), "sync-parent-after-remove", durabilityHook);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) { cleanupErrors.push(error); }
|
|
1270
|
+
}
|
|
1271
|
+
if (cleanupErrors.length > 0) {
|
|
1272
|
+
throw new AggregateError(
|
|
1273
|
+
operationError === undefined
|
|
1274
|
+
? cleanupErrors
|
|
1275
|
+
: [operationError, ...cleanupErrors],
|
|
1276
|
+
"agenc: runtime install and cleanup did not both complete",
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
177
1279
|
}
|
|
178
1280
|
}
|