@tetsuo-ai/agenc 0.1.0 → 0.2.1

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.
@@ -1,831 +0,0 @@
1
- import { createHash, verify } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
- import {
4
- copyFile,
5
- mkdir,
6
- mkdtemp,
7
- open,
8
- readFile,
9
- rename,
10
- rm,
11
- stat,
12
- symlink,
13
- unlink,
14
- writeFile,
15
- } from "node:fs/promises";
16
- import os from "node:os";
17
- import path from "node:path";
18
- import process from "node:process";
19
- import { execFileSync, spawn } from "node:child_process";
20
- import { fileURLToPath } from "node:url";
21
-
22
- const GENERATED_DIRNAME = "generated";
23
- const GENERATED_MANIFEST_BASENAME = "agenc-runtime-manifest.json";
24
- const GENERATED_SIGNATURE_BASENAME = "agenc-runtime-manifest.json.sig";
25
- const GENERATED_PUBLIC_KEY_BASENAME = "agenc-runtime-public-key.pem";
26
- const GENERATED_TRUST_POLICY_BASENAME = "agenc-runtime-trust-policy.json";
27
- const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
28
- const LOCK_POLL_INTERVAL_MS = 100;
29
- const SUPPORTED_PLATFORM_ARCH = new Set(["linux-x64"]);
30
-
31
- export class RuntimeInstallError extends Error {
32
- constructor(message, code = "runtime_install_error") {
33
- super(message);
34
- this.name = "RuntimeInstallError";
35
- this.code = code;
36
- }
37
- }
38
-
39
- function sleep(ms) {
40
- return new Promise((resolve) => setTimeout(resolve, ms));
41
- }
42
-
43
- function parseOptionalString(value) {
44
- return typeof value === "string" && value.trim().length > 0
45
- ? value.trim()
46
- : undefined;
47
- }
48
-
49
- function computeSha256(buffer) {
50
- return createHash("sha256").update(buffer).digest("hex");
51
- }
52
-
53
- function toPlatformArch(platform, arch) {
54
- return `${platform}-${arch}`;
55
- }
56
-
57
- function parseVersion(version) {
58
- const match = /^v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/u.exec(version);
59
- if (!match?.groups) {
60
- return null;
61
- }
62
- return [
63
- Number.parseInt(match.groups.major, 10),
64
- Number.parseInt(match.groups.minor, 10),
65
- Number.parseInt(match.groups.patch, 10),
66
- ];
67
- }
68
-
69
- function compareVersions(left, right) {
70
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
71
- const leftValue = left[index] ?? 0;
72
- const rightValue = right[index] ?? 0;
73
- if (leftValue !== rightValue) {
74
- return leftValue - rightValue;
75
- }
76
- }
77
- return 0;
78
- }
79
-
80
- function satisfiesNodeRange(nodeVersion, range) {
81
- const parsedNodeVersion = parseVersion(nodeVersion);
82
- if (!parsedNodeVersion) {
83
- return false;
84
- }
85
- const trimmedRange = range.trim();
86
- if (trimmedRange.startsWith(">=")) {
87
- const required = parseVersion(trimmedRange.slice(2));
88
- return required ? compareVersions(parsedNodeVersion, required) >= 0 : false;
89
- }
90
- const exact = parseVersion(trimmedRange);
91
- return exact ? compareVersions(parsedNodeVersion, exact) === 0 : false;
92
- }
93
-
94
- function packageRootFromMeta(metaUrl) {
95
- return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..");
96
- }
97
-
98
- export function getPackageRoot(metaUrl = import.meta.url) {
99
- return packageRootFromMeta(metaUrl);
100
- }
101
-
102
- async function readJsonAbsolute(filePath) {
103
- return JSON.parse(await readFile(filePath, "utf8"));
104
- }
105
-
106
- async function getWrapperVersion(packageRoot) {
107
- const packageJsonPath = path.join(packageRoot, "package.json");
108
- const packageJson = await readJsonAbsolute(packageJsonPath);
109
- const version = parseOptionalString(packageJson.version);
110
- if (!version) {
111
- throw new RuntimeInstallError(
112
- `wrapper package version missing at ${packageJsonPath}`,
113
- "invalid_wrapper_package",
114
- );
115
- }
116
- return version;
117
- }
118
-
119
- export function getOperatorHome(homeDir = os.homedir()) {
120
- return path.join(homeDir, ".agenc");
121
- }
122
-
123
- export function getWrapperRuntimeHome(homeDir = os.homedir()) {
124
- return path.join(getOperatorHome(homeDir), "runtime");
125
- }
126
-
127
- export function getGeneratedManifestPaths(packageRoot = getPackageRoot()) {
128
- return {
129
- manifestPath: path.join(
130
- packageRoot,
131
- GENERATED_DIRNAME,
132
- GENERATED_MANIFEST_BASENAME,
133
- ),
134
- signaturePath: path.join(
135
- packageRoot,
136
- GENERATED_DIRNAME,
137
- GENERATED_SIGNATURE_BASENAME,
138
- ),
139
- publicKeyPath: path.join(
140
- packageRoot,
141
- GENERATED_DIRNAME,
142
- GENERATED_PUBLIC_KEY_BASENAME,
143
- ),
144
- trustPolicyPath: path.join(
145
- packageRoot,
146
- GENERATED_DIRNAME,
147
- GENERATED_TRUST_POLICY_BASENAME,
148
- ),
149
- };
150
- }
151
-
152
- function resolveManifestSource({
153
- packageRoot = getPackageRoot(),
154
- env = process.env,
155
- }) {
156
- const manifestPathOverride = parseOptionalString(env.AGENC_RUNTIME_MANIFEST_FILE);
157
- if (manifestPathOverride) {
158
- const manifestPath = path.resolve(manifestPathOverride);
159
- return {
160
- manifestPath,
161
- signaturePath: path.resolve(
162
- parseOptionalString(env.AGENC_RUNTIME_SIGNATURE_FILE) ??
163
- `${manifestPath}.sig`,
164
- ),
165
- publicKeyPath: path.resolve(
166
- parseOptionalString(env.AGENC_RUNTIME_PUBLIC_KEY_FILE) ??
167
- getGeneratedManifestPaths(packageRoot).publicKeyPath,
168
- ),
169
- trustPolicyPath: path.resolve(
170
- parseOptionalString(env.AGENC_RUNTIME_TRUST_POLICY_FILE) ??
171
- getGeneratedManifestPaths(packageRoot).trustPolicyPath,
172
- ),
173
- description: `local manifest ${manifestPath}`,
174
- embedded: false,
175
- };
176
- }
177
-
178
- const embedded = getGeneratedManifestPaths(packageRoot);
179
- if (
180
- existsSync(embedded.manifestPath) &&
181
- existsSync(embedded.signaturePath) &&
182
- existsSync(embedded.publicKeyPath) &&
183
- existsSync(embedded.trustPolicyPath)
184
- ) {
185
- return {
186
- ...embedded,
187
- description: "embedded package manifest",
188
- embedded: true,
189
- };
190
- }
191
-
192
- return null;
193
- }
194
-
195
- function validateManifestShape(manifest, manifestPath) {
196
- if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
197
- throw new RuntimeInstallError(
198
- `runtime manifest at ${manifestPath} must be a JSON object`,
199
- "invalid_manifest",
200
- );
201
- }
202
- if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
203
- throw new RuntimeInstallError(
204
- `runtime manifest at ${manifestPath} must include at least one artifact`,
205
- "invalid_manifest",
206
- );
207
- }
208
- if (typeof manifest.wrapperVersion !== "string" || manifest.wrapperVersion.length === 0) {
209
- throw new RuntimeInstallError(
210
- `runtime manifest at ${manifestPath} must declare wrapperVersion`,
211
- "invalid_manifest",
212
- );
213
- }
214
- if (typeof manifest.keyId !== "string" || manifest.keyId.length === 0) {
215
- throw new RuntimeInstallError(
216
- `runtime manifest at ${manifestPath} must declare keyId`,
217
- "invalid_manifest",
218
- );
219
- }
220
- }
221
-
222
- function validateTrustPolicyShape(trustPolicy, trustPolicyPath) {
223
- if (
224
- typeof trustPolicy !== "object" ||
225
- trustPolicy === null ||
226
- Array.isArray(trustPolicy)
227
- ) {
228
- throw new RuntimeInstallError(
229
- `runtime trust policy at ${trustPolicyPath} must be a JSON object`,
230
- "invalid_trust_policy",
231
- );
232
- }
233
- if (
234
- typeof trustPolicy.wrapperVersion !== "string" ||
235
- trustPolicy.wrapperVersion.length === 0
236
- ) {
237
- throw new RuntimeInstallError(
238
- `runtime trust policy at ${trustPolicyPath} must declare wrapperVersion`,
239
- "invalid_trust_policy",
240
- );
241
- }
242
- if (typeof trustPolicy.keyId !== "string" || trustPolicy.keyId.length === 0) {
243
- throw new RuntimeInstallError(
244
- `runtime trust policy at ${trustPolicyPath} must declare keyId`,
245
- "invalid_trust_policy",
246
- );
247
- }
248
- if (
249
- !Array.isArray(trustPolicy.revokedManifestDigests) ||
250
- !Array.isArray(trustPolicy.revokedRuntimeVersions)
251
- ) {
252
- throw new RuntimeInstallError(
253
- `runtime trust policy at ${trustPolicyPath} must declare revokedManifestDigests and revokedRuntimeVersions arrays`,
254
- "invalid_trust_policy",
255
- );
256
- }
257
- }
258
-
259
- export async function loadVerifiedManifest(options = {}) {
260
- const packageRoot = options.packageRoot ?? getPackageRoot();
261
- const env = options.env ?? process.env;
262
- const source = resolveManifestSource({ packageRoot, env });
263
- if (!source) {
264
- return null;
265
- }
266
-
267
- const wrapperVersion =
268
- options.wrapperVersion ?? (await getWrapperVersion(packageRoot));
269
- const manifestBytes = await readFile(source.manifestPath);
270
- const manifestDigest = computeSha256(manifestBytes);
271
- const signatureText = (await readFile(source.signaturePath, "utf8")).trim();
272
- const publicKeyPem = await readFile(source.publicKeyPath, "utf8");
273
- const trustPolicy = JSON.parse(await readFile(source.trustPolicyPath, "utf8"));
274
- validateTrustPolicyShape(trustPolicy, source.trustPolicyPath);
275
-
276
- const signature = Buffer.from(signatureText, "base64");
277
- const verified = verify(null, manifestBytes, publicKeyPem, signature);
278
- if (!verified) {
279
- throw new RuntimeInstallError(
280
- `runtime manifest signature verification failed for ${source.manifestPath}`,
281
- "invalid_manifest_signature",
282
- );
283
- }
284
-
285
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
286
- validateManifestShape(manifest, source.manifestPath);
287
-
288
- if (
289
- manifest.wrapperVersion !== wrapperVersion ||
290
- trustPolicy.wrapperVersion !== wrapperVersion
291
- ) {
292
- throw new RuntimeInstallError(
293
- `runtime manifest wrapperVersion ${manifest.wrapperVersion} is incompatible with wrapper package ${wrapperVersion}`,
294
- "incompatible_wrapper_version",
295
- );
296
- }
297
- if (manifest.keyId !== trustPolicy.keyId) {
298
- throw new RuntimeInstallError(
299
- `runtime manifest keyId ${manifest.keyId} does not match trust policy keyId ${trustPolicy.keyId}`,
300
- "unexpected_manifest_key",
301
- );
302
- }
303
- if (trustPolicy.revokedManifestDigests.includes(manifestDigest)) {
304
- throw new RuntimeInstallError(
305
- `runtime manifest ${manifestDigest} has been revoked`,
306
- "revoked_manifest",
307
- );
308
- }
309
- const runtimeVersions = new Set(
310
- manifest.artifacts
311
- .map((artifact) => artifact?.runtimeVersion)
312
- .filter((value) => typeof value === "string"),
313
- );
314
- for (const runtimeVersion of runtimeVersions) {
315
- if (trustPolicy.revokedRuntimeVersions.includes(runtimeVersion)) {
316
- throw new RuntimeInstallError(
317
- `runtime version ${runtimeVersion} has been revoked`,
318
- "revoked_runtime_version",
319
- );
320
- }
321
- }
322
-
323
- return {
324
- manifest,
325
- source,
326
- manifestDigest,
327
- trustPolicy,
328
- wrapperVersion,
329
- };
330
- }
331
-
332
- export function selectArtifactEntry(
333
- manifest,
334
- {
335
- platform = process.platform,
336
- arch = process.arch,
337
- nodeVersion = process.versions.node,
338
- } = {},
339
- ) {
340
- const platformArch = toPlatformArch(platform, arch);
341
- if (!SUPPORTED_PLATFORM_ARCH.has(platformArch)) {
342
- throw new RuntimeInstallError(
343
- `unsupported platform ${platformArch}; supported targets: ${Array.from(SUPPORTED_PLATFORM_ARCH).sort().join(", ")}`,
344
- "unsupported_platform",
345
- );
346
- }
347
-
348
- const selected = manifest.artifacts.find(
349
- (artifact) => artifact.platform === platform && artifact.arch === arch,
350
- );
351
- if (!selected) {
352
- const supported = manifest.artifacts
353
- .map((artifact) => toPlatformArch(artifact.platform, artifact.arch))
354
- .sort()
355
- .join(", ");
356
- throw new RuntimeInstallError(
357
- `runtime manifest does not contain an artifact for ${platformArch}; available: ${supported || "none"}`,
358
- "missing_platform_artifact",
359
- );
360
- }
361
- if (
362
- typeof selected.nodeRange === "string" &&
363
- !satisfiesNodeRange(nodeVersion, selected.nodeRange)
364
- ) {
365
- throw new RuntimeInstallError(
366
- `runtime artifact for ${platformArch} requires Node ${selected.nodeRange}; current Node is ${nodeVersion}`,
367
- "unsupported_node",
368
- );
369
- }
370
- return selected;
371
- }
372
-
373
- function getInstallPaths(selectedArtifact, homeDir = os.homedir()) {
374
- const runtimeHome = getWrapperRuntimeHome(homeDir);
375
- const releaseDir = path.join(
376
- runtimeHome,
377
- "releases",
378
- selectedArtifact.runtimeVersion,
379
- toPlatformArch(selectedArtifact.platform, selectedArtifact.arch),
380
- );
381
- return {
382
- runtimeHome,
383
- releaseDir,
384
- currentDir: path.join(runtimeHome, "current"),
385
- statePath: path.join(runtimeHome, "install-state.json"),
386
- lockPath: path.join(runtimeHome, ".install.lock"),
387
- tempRoot: path.join(runtimeHome, "tmp"),
388
- };
389
- }
390
-
391
- async function readJsonIfExists(filePath) {
392
- try {
393
- const raw = await readFile(filePath, "utf8");
394
- return JSON.parse(raw);
395
- } catch (error) {
396
- if (
397
- error &&
398
- typeof error === "object" &&
399
- "code" in error &&
400
- error.code === "ENOENT"
401
- ) {
402
- return null;
403
- }
404
- throw error;
405
- }
406
- }
407
-
408
- async function acquireLock(lockPath, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
409
- const start = Date.now();
410
- await mkdir(path.dirname(lockPath), { recursive: true });
411
-
412
- while (true) {
413
- try {
414
- return await open(lockPath, "wx");
415
- } catch (error) {
416
- if (
417
- !(
418
- error &&
419
- typeof error === "object" &&
420
- "code" in error &&
421
- error.code === "EEXIST"
422
- )
423
- ) {
424
- throw error;
425
- }
426
- if (Date.now() - start >= timeoutMs) {
427
- throw new RuntimeInstallError(
428
- `timed out waiting for runtime install lock at ${lockPath}`,
429
- "install_lock_timeout",
430
- );
431
- }
432
- await sleep(LOCK_POLL_INTERVAL_MS);
433
- }
434
- }
435
- }
436
-
437
- async function withInstallLock(lockPath, callback) {
438
- const handle = await acquireLock(lockPath);
439
- try {
440
- return await callback();
441
- } finally {
442
- await handle.close().catch(() => {});
443
- await unlink(lockPath).catch(() => {});
444
- }
445
- }
446
-
447
- async function downloadArtifact(urlString, destinationPath) {
448
- const url = new URL(urlString);
449
- if (url.protocol === "file:") {
450
- await copyFile(fileURLToPath(url), destinationPath);
451
- return;
452
- }
453
- if (url.protocol !== "https:") {
454
- throw new RuntimeInstallError(
455
- `unsupported artifact URL protocol for ${urlString}`,
456
- "unsupported_artifact_protocol",
457
- );
458
- }
459
-
460
- const response = await fetch(url);
461
- if (!response.ok) {
462
- throw new RuntimeInstallError(
463
- `failed to download runtime artifact ${urlString}: ${response.status} ${response.statusText}`,
464
- "artifact_download_failed",
465
- );
466
- }
467
- const bytes = Buffer.from(await response.arrayBuffer());
468
- await writeFile(destinationPath, bytes);
469
- }
470
-
471
- function extractTarball(tarballPath, destinationDir) {
472
- execFileSync("tar", ["-xzf", tarballPath, "-C", destinationDir], {
473
- stdio: ["ignore", "pipe", "pipe"],
474
- });
475
- }
476
-
477
- function getBinPathForArtifact(rootDir, selectedArtifact, binName) {
478
- const binRelativePath = selectedArtifact?.bins?.[binName];
479
- if (typeof binRelativePath !== "string" || binRelativePath.length === 0) {
480
- throw new RuntimeInstallError(
481
- `runtime artifact does not provide a ${binName} bin`,
482
- "missing_runtime_bin",
483
- );
484
- }
485
- return path.join(rootDir, binRelativePath);
486
- }
487
-
488
- async function installationIsCurrent(installPaths, selectedArtifact) {
489
- try {
490
- const metadata = await readJsonIfExists(
491
- path.join(installPaths.releaseDir, "agenc-runtime-installation.json"),
492
- );
493
- if (!metadata) {
494
- return false;
495
- }
496
- if (
497
- metadata.sha256 !== selectedArtifact.sha256 ||
498
- metadata.url !== selectedArtifact.url ||
499
- metadata.runtimeVersion !== selectedArtifact.runtimeVersion
500
- ) {
501
- return false;
502
- }
503
- const agencBin = getBinPathForArtifact(
504
- installPaths.releaseDir,
505
- selectedArtifact,
506
- "agenc",
507
- );
508
- const runtimeBin = getBinPathForArtifact(
509
- installPaths.releaseDir,
510
- selectedArtifact,
511
- "agenc-runtime",
512
- );
513
- await stat(agencBin);
514
- await stat(runtimeBin);
515
- return true;
516
- } catch {
517
- return false;
518
- }
519
- }
520
-
521
- async function ensureCurrentPointer(installPaths) {
522
- await mkdir(path.dirname(installPaths.currentDir), { recursive: true });
523
- await rm(installPaths.currentDir, { recursive: true, force: true });
524
- await symlink(installPaths.releaseDir, installPaths.currentDir, "dir");
525
- }
526
-
527
- async function writeInstallState(statePath, payload) {
528
- await mkdir(path.dirname(statePath), { recursive: true });
529
- await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
530
- }
531
-
532
- function getStableManagedBinPaths(installPaths, selectedArtifact) {
533
- return {
534
- agenc: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "agenc"),
535
- "agenc-runtime": getBinPathForArtifact(
536
- installPaths.currentDir,
537
- selectedArtifact,
538
- "agenc-runtime",
539
- ),
540
- daemon: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "daemon"),
541
- watch: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "agenc-watch"),
542
- };
543
- }
544
-
545
- async function getDaemonPidInfo(homeDir) {
546
- const pidPath = path.join(getOperatorHome(homeDir), "daemon.pid");
547
- const pidInfo = await readJsonIfExists(pidPath);
548
- if (
549
- pidInfo &&
550
- typeof pidInfo.pid === "number" &&
551
- Number.isInteger(pidInfo.pid) &&
552
- pidInfo.pid > 0
553
- ) {
554
- try {
555
- process.kill(pidInfo.pid, 0);
556
- return pidInfo;
557
- } catch {
558
- return null;
559
- }
560
- }
561
- return null;
562
- }
563
-
564
- export async function ensureRuntimeInstalled(options = {}) {
565
- const packageRoot = options.packageRoot ?? getPackageRoot();
566
- const env = options.env ?? process.env;
567
- const homeDir = options.homeDir ?? os.homedir();
568
- const verifiedManifest = await loadVerifiedManifest({ packageRoot, env });
569
- if (!verifiedManifest) {
570
- throw new RuntimeInstallError(
571
- "no embedded or overridden runtime manifest is available; install a prepared agenc package or provide AGENC_RUNTIME_MANIFEST_FILE for local development",
572
- "missing_manifest",
573
- );
574
- }
575
-
576
- const selectedArtifact = selectArtifactEntry(verifiedManifest.manifest, {
577
- platform: options.platform,
578
- arch: options.arch,
579
- nodeVersion: options.nodeVersion,
580
- });
581
- const installPaths = getInstallPaths(selectedArtifact, homeDir);
582
- if (
583
- !options.force &&
584
- (await installationIsCurrent(installPaths, selectedArtifact))
585
- ) {
586
- await ensureCurrentPointer(installPaths);
587
- return {
588
- releaseDir: installPaths.releaseDir,
589
- currentDir: installPaths.currentDir,
590
- runtimeHome: installPaths.runtimeHome,
591
- statePath: installPaths.statePath,
592
- manifestSource: verifiedManifest.source.description,
593
- selectedArtifact,
594
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
595
- };
596
- }
597
-
598
- return withInstallLock(installPaths.lockPath, async () => {
599
- if (
600
- !options.force &&
601
- (await installationIsCurrent(installPaths, selectedArtifact))
602
- ) {
603
- await ensureCurrentPointer(installPaths);
604
- return {
605
- releaseDir: installPaths.releaseDir,
606
- currentDir: installPaths.currentDir,
607
- runtimeHome: installPaths.runtimeHome,
608
- statePath: installPaths.statePath,
609
- manifestSource: verifiedManifest.source.description,
610
- selectedArtifact,
611
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
612
- };
613
- }
614
-
615
- await mkdir(installPaths.tempRoot, { recursive: true });
616
- const tempDir = await mkdtemp(path.join(installPaths.tempRoot, "install-"));
617
- const tarballPath = path.join(tempDir, "runtime.tar.gz");
618
- const extractDir = path.join(tempDir, "extract");
619
- await mkdir(extractDir, { recursive: true });
620
-
621
- try {
622
- await downloadArtifact(selectedArtifact.url, tarballPath);
623
- const tarballBytes = await readFile(tarballPath);
624
- const actualSha = computeSha256(tarballBytes);
625
- if (actualSha !== selectedArtifact.sha256) {
626
- throw new RuntimeInstallError(
627
- `runtime artifact checksum mismatch for ${selectedArtifact.url}`,
628
- "artifact_checksum_mismatch",
629
- );
630
- }
631
-
632
- extractTarball(tarballPath, extractDir);
633
- const installMetadataPath = path.join(
634
- extractDir,
635
- "agenc-runtime-installation.json",
636
- );
637
- await writeFile(
638
- installMetadataPath,
639
- `${JSON.stringify(
640
- {
641
- installedAt: new Date().toISOString(),
642
- runtimeVersion: selectedArtifact.runtimeVersion,
643
- platform: selectedArtifact.platform,
644
- arch: selectedArtifact.arch,
645
- sha256: selectedArtifact.sha256,
646
- url: selectedArtifact.url,
647
- bins: selectedArtifact.bins,
648
- wrapperVersion: verifiedManifest.wrapperVersion,
649
- manifestDigest: verifiedManifest.manifestDigest,
650
- keyId: verifiedManifest.manifest.keyId,
651
- },
652
- null,
653
- 2,
654
- )}\n`,
655
- "utf8",
656
- );
657
-
658
- await mkdir(path.dirname(installPaths.releaseDir), { recursive: true });
659
- await rm(installPaths.releaseDir, { recursive: true, force: true });
660
- await rename(extractDir, installPaths.releaseDir);
661
- await ensureCurrentPointer(installPaths);
662
- await writeInstallState(installPaths.statePath, {
663
- installedAt: new Date().toISOString(),
664
- manifestSource: verifiedManifest.source.description,
665
- manifestDigest: verifiedManifest.manifestDigest,
666
- runtimeVersion: selectedArtifact.runtimeVersion,
667
- platform: selectedArtifact.platform,
668
- arch: selectedArtifact.arch,
669
- releaseDir: installPaths.releaseDir,
670
- currentDir: installPaths.currentDir,
671
- });
672
- } finally {
673
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
674
- }
675
-
676
- return {
677
- releaseDir: installPaths.releaseDir,
678
- currentDir: installPaths.currentDir,
679
- runtimeHome: installPaths.runtimeHome,
680
- statePath: installPaths.statePath,
681
- manifestSource: verifiedManifest.source.description,
682
- selectedArtifact,
683
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
684
- };
685
- });
686
- }
687
-
688
- export async function uninstallRuntime(options = {}) {
689
- const homeDir = options.homeDir ?? os.homedir();
690
- const force = options.force === true;
691
- const daemonPidInfo = await getDaemonPidInfo(homeDir);
692
- if (daemonPidInfo && !force) {
693
- throw new RuntimeInstallError(
694
- `refusing to uninstall while daemon ${daemonPidInfo.pid} is still running; stop AgenC first or rerun with --force`,
695
- "daemon_still_running",
696
- );
697
- }
698
-
699
- const packageRoot = options.packageRoot ?? getPackageRoot();
700
- const env = options.env ?? process.env;
701
- const verifiedManifest = await loadVerifiedManifest({ packageRoot, env }).catch(
702
- () => null,
703
- );
704
- const selectedArtifact = verifiedManifest
705
- ? selectArtifactEntry(verifiedManifest.manifest, {
706
- platform: options.platform,
707
- arch: options.arch,
708
- nodeVersion: options.nodeVersion,
709
- })
710
- : null;
711
-
712
- const runtimeHome = getWrapperRuntimeHome(homeDir);
713
- const statePath = path.join(runtimeHome, "install-state.json");
714
- const currentDir = path.join(runtimeHome, "current");
715
- const installDir = selectedArtifact
716
- ? getInstallPaths(selectedArtifact, homeDir).releaseDir
717
- : (await readJsonIfExists(statePath))?.releaseDir;
718
-
719
- if (!installDir) {
720
- return {
721
- removed: false,
722
- runtimeHome,
723
- releaseDir: null,
724
- preservedPaths: [getOperatorHome(homeDir)],
725
- };
726
- }
727
-
728
- await rm(installDir, { recursive: true, force: true });
729
- await rm(currentDir, { recursive: true, force: true });
730
- const state = await readJsonIfExists(statePath);
731
- if (state && state.releaseDir === installDir) {
732
- await rm(statePath, { force: true });
733
- }
734
- return {
735
- removed: true,
736
- runtimeHome,
737
- releaseDir: installDir,
738
- preservedPaths: [
739
- path.join(getOperatorHome(homeDir), "config.json"),
740
- path.join(getOperatorHome(homeDir), "daemon.pid"),
741
- path.join(getOperatorHome(homeDir), "replay-events.sqlite"),
742
- getOperatorHome(homeDir),
743
- ],
744
- };
745
- }
746
-
747
- export async function describeRuntimeInstall(options = {}) {
748
- const homeDir = options.homeDir ?? os.homedir();
749
- const packageRoot = options.packageRoot ?? getPackageRoot();
750
- const env = options.env ?? process.env;
751
- const runtimeHome = getWrapperRuntimeHome(homeDir);
752
- const statePath = path.join(runtimeHome, "install-state.json");
753
- const state = await readJsonIfExists(statePath);
754
- const manifest = await loadVerifiedManifest({ packageRoot, env }).catch(
755
- () => null,
756
- );
757
- const selectedArtifact = manifest
758
- ? selectArtifactEntry(manifest.manifest, {
759
- platform: options.platform,
760
- arch: options.arch,
761
- nodeVersion: options.nodeVersion,
762
- })
763
- : null;
764
-
765
- return {
766
- runtimeHome,
767
- currentDir: path.join(runtimeHome, "current"),
768
- statePath,
769
- installed: state !== null && typeof state.releaseDir === "string",
770
- releaseDir: state?.releaseDir ?? null,
771
- manifestSource: manifest?.source.description ?? null,
772
- manifestDigest: manifest?.manifestDigest ?? null,
773
- selectedArtifact,
774
- trustPolicy: manifest?.trustPolicy ?? null,
775
- };
776
- }
777
-
778
- async function spawnNodeScript(scriptPath, args, env, cwd) {
779
- return new Promise((resolve, reject) => {
780
- const child = spawn(process.execPath, [scriptPath, ...args], {
781
- stdio: "inherit",
782
- env,
783
- cwd,
784
- });
785
- child.on("error", reject);
786
- child.on("exit", (code, signal) => {
787
- if (signal) {
788
- resolve(1);
789
- return;
790
- }
791
- resolve(code ?? 0);
792
- });
793
- });
794
- }
795
-
796
- export async function spawnInstalledRuntimeBin(
797
- binName,
798
- args,
799
- options = {},
800
- ) {
801
- const runtime = await ensureRuntimeInstalled(options);
802
- const scriptPath = runtime.bins[binName];
803
- if (!scriptPath) {
804
- throw new RuntimeInstallError(
805
- `installed runtime does not expose ${binName}`,
806
- "missing_runtime_bin",
807
- );
808
- }
809
- const env = {
810
- ...(options.env ?? process.env),
811
- AGENC_WRAPPER_ACTIVE: "1",
812
- AGENC_WRAPPER_RUNTIME_HOME: runtime.runtimeHome,
813
- AGENC_WRAPPER_RELEASE_DIR: runtime.releaseDir,
814
- AGENC_WRAPPER_CURRENT_DIR: runtime.currentDir,
815
- AGENC_DAEMON_ENTRY: runtime.bins.daemon,
816
- AGENC_WATCH_ENTRY: runtime.bins.watch,
817
- };
818
- return spawnNodeScript(scriptPath, args, env, options.cwd);
819
- }
820
-
821
- export async function prefetchRuntimeOnInstall(options = {}) {
822
- const manifest = await loadVerifiedManifest(options);
823
- if (!manifest) {
824
- return { prefetched: false, reason: "missing_manifest" };
825
- }
826
- await ensureRuntimeInstalled({
827
- ...options,
828
- force: false,
829
- });
830
- return { prefetched: true };
831
- }