@tetsuo-ai/agenc 0.2.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,1111 +0,0 @@
1
- import { createHash, verify } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import {
4
- copyFile,
5
- lstat,
6
- mkdir,
7
- mkdtemp,
8
- open,
9
- readFile,
10
- rename,
11
- rm,
12
- stat,
13
- symlink,
14
- unlink,
15
- writeFile,
16
- } from "node:fs/promises";
17
- import os from "node:os";
18
- import path from "node:path";
19
- import process from "node:process";
20
- import { execFileSync, spawn } from "node:child_process";
21
- import { fileURLToPath } from "node:url";
22
-
23
- const GENERATED_DIRNAME = "generated";
24
- const GENERATED_MANIFEST_BASENAME = "agenc-runtime-manifest.json";
25
- const GENERATED_SIGNATURE_BASENAME = "agenc-runtime-manifest.json.sig";
26
- const GENERATED_PUBLIC_KEY_BASENAME = "agenc-runtime-public-key.pem";
27
- const GENERATED_TRUST_POLICY_BASENAME = "agenc-runtime-trust-policy.json";
28
- const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
29
- const LOCK_POLL_INTERVAL_MS = 100;
30
- const SUPPORTED_PLATFORM_ARCH = new Set(["linux-x64"]);
31
-
32
- export class RuntimeInstallError extends Error {
33
- constructor(message, code = "runtime_install_error") {
34
- super(message);
35
- this.name = "RuntimeInstallError";
36
- this.code = code;
37
- }
38
- }
39
-
40
- /**
41
- * Remove a filesystem path regardless of whether it's a symlink, a
42
- * regular file, or a directory. `fs.rm` with `recursive: true`
43
- * follows symlinks to directories (removing the TARGET, not the
44
- * link), so we `lstat` first and use `unlink` for the symlink case.
45
- */
46
- async function removePathSafe(targetPath) {
47
- let info;
48
- try {
49
- info = await lstat(targetPath);
50
- } catch (error) {
51
- if ((error && error.code) === "ENOENT") return;
52
- throw error;
53
- }
54
- if (info.isSymbolicLink() || info.isFile()) {
55
- await unlink(targetPath);
56
- return;
57
- }
58
- await rm(targetPath, { recursive: true, force: true });
59
- }
60
-
61
- function sleep(ms) {
62
- return new Promise((resolve) => setTimeout(resolve, ms));
63
- }
64
-
65
- function parseOptionalString(value) {
66
- return typeof value === "string" && value.trim().length > 0
67
- ? value.trim()
68
- : undefined;
69
- }
70
-
71
- function computeSha256(buffer) {
72
- return createHash("sha256").update(buffer).digest("hex");
73
- }
74
-
75
- function toPlatformArch(platform, arch) {
76
- return `${platform}-${arch}`;
77
- }
78
-
79
- function parseVersion(version) {
80
- const match = /^v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/u.exec(version);
81
- if (!match?.groups) {
82
- return null;
83
- }
84
- return [
85
- Number.parseInt(match.groups.major, 10),
86
- Number.parseInt(match.groups.minor, 10),
87
- Number.parseInt(match.groups.patch, 10),
88
- ];
89
- }
90
-
91
- function compareVersions(left, right) {
92
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
93
- const leftValue = left[index] ?? 0;
94
- const rightValue = right[index] ?? 0;
95
- if (leftValue !== rightValue) {
96
- return leftValue - rightValue;
97
- }
98
- }
99
- return 0;
100
- }
101
-
102
- function satisfiesNodeRange(nodeVersion, range) {
103
- const parsedNodeVersion = parseVersion(nodeVersion);
104
- if (!parsedNodeVersion) {
105
- return false;
106
- }
107
- const trimmedRange = range.trim();
108
- if (trimmedRange.startsWith(">=")) {
109
- const required = parseVersion(trimmedRange.slice(2));
110
- return required ? compareVersions(parsedNodeVersion, required) >= 0 : false;
111
- }
112
- const exact = parseVersion(trimmedRange);
113
- return exact ? compareVersions(parsedNodeVersion, exact) === 0 : false;
114
- }
115
-
116
- function packageRootFromMeta(metaUrl) {
117
- return path.resolve(path.dirname(fileURLToPath(metaUrl)), "..");
118
- }
119
-
120
- export function getPackageRoot(metaUrl = import.meta.url) {
121
- return packageRootFromMeta(metaUrl);
122
- }
123
-
124
- async function readJsonAbsolute(filePath) {
125
- return JSON.parse(await readFile(filePath, "utf8"));
126
- }
127
-
128
- async function getWrapperVersion(packageRoot) {
129
- const packageJsonPath = path.join(packageRoot, "package.json");
130
- const packageJson = await readJsonAbsolute(packageJsonPath);
131
- const version = parseOptionalString(packageJson.version);
132
- if (!version) {
133
- throw new RuntimeInstallError(
134
- `wrapper package version missing at ${packageJsonPath}`,
135
- "invalid_wrapper_package",
136
- );
137
- }
138
- return version;
139
- }
140
-
141
- export function getOperatorHome(homeDir = os.homedir()) {
142
- return path.join(homeDir, ".agenc");
143
- }
144
-
145
- export function getWrapperRuntimeHome(homeDir = os.homedir()) {
146
- return path.join(getOperatorHome(homeDir), "runtime");
147
- }
148
-
149
- export function getGeneratedManifestPaths(packageRoot = getPackageRoot()) {
150
- return {
151
- manifestPath: path.join(
152
- packageRoot,
153
- GENERATED_DIRNAME,
154
- GENERATED_MANIFEST_BASENAME,
155
- ),
156
- signaturePath: path.join(
157
- packageRoot,
158
- GENERATED_DIRNAME,
159
- GENERATED_SIGNATURE_BASENAME,
160
- ),
161
- publicKeyPath: path.join(
162
- packageRoot,
163
- GENERATED_DIRNAME,
164
- GENERATED_PUBLIC_KEY_BASENAME,
165
- ),
166
- trustPolicyPath: path.join(
167
- packageRoot,
168
- GENERATED_DIRNAME,
169
- GENERATED_TRUST_POLICY_BASENAME,
170
- ),
171
- };
172
- }
173
-
174
- function resolveManifestSource({
175
- packageRoot = getPackageRoot(),
176
- env = process.env,
177
- }) {
178
- const manifestPathOverride = parseOptionalString(env.AGENC_RUNTIME_MANIFEST_FILE);
179
- if (manifestPathOverride) {
180
- const manifestPath = path.resolve(manifestPathOverride);
181
- return {
182
- manifestPath,
183
- signaturePath: path.resolve(
184
- parseOptionalString(env.AGENC_RUNTIME_SIGNATURE_FILE) ??
185
- `${manifestPath}.sig`,
186
- ),
187
- publicKeyPath: path.resolve(
188
- parseOptionalString(env.AGENC_RUNTIME_PUBLIC_KEY_FILE) ??
189
- getGeneratedManifestPaths(packageRoot).publicKeyPath,
190
- ),
191
- trustPolicyPath: path.resolve(
192
- parseOptionalString(env.AGENC_RUNTIME_TRUST_POLICY_FILE) ??
193
- getGeneratedManifestPaths(packageRoot).trustPolicyPath,
194
- ),
195
- description: `local manifest ${manifestPath}`,
196
- embedded: false,
197
- };
198
- }
199
-
200
- const embedded = getGeneratedManifestPaths(packageRoot);
201
- if (
202
- existsSync(embedded.manifestPath) &&
203
- existsSync(embedded.signaturePath) &&
204
- existsSync(embedded.publicKeyPath) &&
205
- existsSync(embedded.trustPolicyPath)
206
- ) {
207
- return {
208
- ...embedded,
209
- description: "embedded package manifest",
210
- embedded: true,
211
- };
212
- }
213
-
214
- return null;
215
- }
216
-
217
- function validateManifestShape(manifest, manifestPath) {
218
- if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
219
- throw new RuntimeInstallError(
220
- `runtime manifest at ${manifestPath} must be a JSON object`,
221
- "invalid_manifest",
222
- );
223
- }
224
- if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
225
- throw new RuntimeInstallError(
226
- `runtime manifest at ${manifestPath} must include at least one artifact`,
227
- "invalid_manifest",
228
- );
229
- }
230
- if (typeof manifest.wrapperVersion !== "string" || manifest.wrapperVersion.length === 0) {
231
- throw new RuntimeInstallError(
232
- `runtime manifest at ${manifestPath} must declare wrapperVersion`,
233
- "invalid_manifest",
234
- );
235
- }
236
- if (typeof manifest.keyId !== "string" || manifest.keyId.length === 0) {
237
- throw new RuntimeInstallError(
238
- `runtime manifest at ${manifestPath} must declare keyId`,
239
- "invalid_manifest",
240
- );
241
- }
242
- }
243
-
244
- function validateTrustPolicyShape(trustPolicy, trustPolicyPath) {
245
- if (
246
- typeof trustPolicy !== "object" ||
247
- trustPolicy === null ||
248
- Array.isArray(trustPolicy)
249
- ) {
250
- throw new RuntimeInstallError(
251
- `runtime trust policy at ${trustPolicyPath} must be a JSON object`,
252
- "invalid_trust_policy",
253
- );
254
- }
255
- if (
256
- typeof trustPolicy.wrapperVersion !== "string" ||
257
- trustPolicy.wrapperVersion.length === 0
258
- ) {
259
- throw new RuntimeInstallError(
260
- `runtime trust policy at ${trustPolicyPath} must declare wrapperVersion`,
261
- "invalid_trust_policy",
262
- );
263
- }
264
- if (typeof trustPolicy.keyId !== "string" || trustPolicy.keyId.length === 0) {
265
- throw new RuntimeInstallError(
266
- `runtime trust policy at ${trustPolicyPath} must declare keyId`,
267
- "invalid_trust_policy",
268
- );
269
- }
270
- if (
271
- !Array.isArray(trustPolicy.revokedManifestDigests) ||
272
- !Array.isArray(trustPolicy.revokedRuntimeVersions)
273
- ) {
274
- throw new RuntimeInstallError(
275
- `runtime trust policy at ${trustPolicyPath} must declare revokedManifestDigests and revokedRuntimeVersions arrays`,
276
- "invalid_trust_policy",
277
- );
278
- }
279
- }
280
-
281
- export async function loadVerifiedManifest(options = {}) {
282
- const packageRoot = options.packageRoot ?? getPackageRoot();
283
- const env = options.env ?? process.env;
284
- const source = resolveManifestSource({ packageRoot, env });
285
- if (!source) {
286
- return null;
287
- }
288
-
289
- const wrapperVersion =
290
- options.wrapperVersion ?? (await getWrapperVersion(packageRoot));
291
- const manifestBytes = await readFile(source.manifestPath);
292
- const manifestDigest = computeSha256(manifestBytes);
293
- const signatureText = (await readFile(source.signaturePath, "utf8")).trim();
294
- const publicKeyPem = await readFile(source.publicKeyPath, "utf8");
295
- const trustPolicy = JSON.parse(await readFile(source.trustPolicyPath, "utf8"));
296
- validateTrustPolicyShape(trustPolicy, source.trustPolicyPath);
297
-
298
- const signature = Buffer.from(signatureText, "base64");
299
- const verified = verify(null, manifestBytes, publicKeyPem, signature);
300
- if (!verified) {
301
- throw new RuntimeInstallError(
302
- `runtime manifest signature verification failed for ${source.manifestPath}`,
303
- "invalid_manifest_signature",
304
- );
305
- }
306
-
307
- const manifest = JSON.parse(manifestBytes.toString("utf8"));
308
- validateManifestShape(manifest, source.manifestPath);
309
-
310
- if (
311
- manifest.wrapperVersion !== wrapperVersion ||
312
- trustPolicy.wrapperVersion !== wrapperVersion
313
- ) {
314
- throw new RuntimeInstallError(
315
- `runtime manifest wrapperVersion ${manifest.wrapperVersion} is incompatible with wrapper package ${wrapperVersion}`,
316
- "incompatible_wrapper_version",
317
- );
318
- }
319
- if (manifest.keyId !== trustPolicy.keyId) {
320
- throw new RuntimeInstallError(
321
- `runtime manifest keyId ${manifest.keyId} does not match trust policy keyId ${trustPolicy.keyId}`,
322
- "unexpected_manifest_key",
323
- );
324
- }
325
- if (trustPolicy.revokedManifestDigests.includes(manifestDigest)) {
326
- throw new RuntimeInstallError(
327
- `runtime manifest ${manifestDigest} has been revoked`,
328
- "revoked_manifest",
329
- );
330
- }
331
- const runtimeVersions = new Set(
332
- manifest.artifacts
333
- .map((artifact) => artifact?.runtimeVersion)
334
- .filter((value) => typeof value === "string"),
335
- );
336
- for (const runtimeVersion of runtimeVersions) {
337
- if (trustPolicy.revokedRuntimeVersions.includes(runtimeVersion)) {
338
- throw new RuntimeInstallError(
339
- `runtime version ${runtimeVersion} has been revoked`,
340
- "revoked_runtime_version",
341
- );
342
- }
343
- }
344
-
345
- return {
346
- manifest,
347
- source,
348
- manifestDigest,
349
- trustPolicy,
350
- wrapperVersion,
351
- };
352
- }
353
-
354
- export function selectArtifactEntry(
355
- manifest,
356
- {
357
- platform = process.platform,
358
- arch = process.arch,
359
- nodeVersion = process.versions.node,
360
- } = {},
361
- ) {
362
- const platformArch = toPlatformArch(platform, arch);
363
- if (!SUPPORTED_PLATFORM_ARCH.has(platformArch)) {
364
- throw new RuntimeInstallError(
365
- `unsupported platform ${platformArch}; supported targets: ${Array.from(SUPPORTED_PLATFORM_ARCH).sort().join(", ")}`,
366
- "unsupported_platform",
367
- );
368
- }
369
-
370
- const selected = manifest.artifacts.find(
371
- (artifact) => artifact.platform === platform && artifact.arch === arch,
372
- );
373
- if (!selected) {
374
- const supported = manifest.artifacts
375
- .map((artifact) => toPlatformArch(artifact.platform, artifact.arch))
376
- .sort()
377
- .join(", ");
378
- throw new RuntimeInstallError(
379
- `runtime manifest does not contain an artifact for ${platformArch}; available: ${supported || "none"}`,
380
- "missing_platform_artifact",
381
- );
382
- }
383
- if (
384
- typeof selected.nodeRange === "string" &&
385
- !satisfiesNodeRange(nodeVersion, selected.nodeRange)
386
- ) {
387
- throw new RuntimeInstallError(
388
- `runtime artifact for ${platformArch} requires Node ${selected.nodeRange}; current Node is ${nodeVersion}`,
389
- "unsupported_node",
390
- );
391
- }
392
- return selected;
393
- }
394
-
395
- function getInstallPaths(selectedArtifact, homeDir = os.homedir()) {
396
- const runtimeHome = getWrapperRuntimeHome(homeDir);
397
- const releaseDir = path.join(
398
- runtimeHome,
399
- "releases",
400
- selectedArtifact.runtimeVersion,
401
- toPlatformArch(selectedArtifact.platform, selectedArtifact.arch),
402
- );
403
- return {
404
- runtimeHome,
405
- releaseDir,
406
- currentDir: path.join(runtimeHome, "current"),
407
- statePath: path.join(runtimeHome, "install-state.json"),
408
- lockPath: path.join(runtimeHome, ".install.lock"),
409
- tempRoot: path.join(runtimeHome, "tmp"),
410
- };
411
- }
412
-
413
- async function readJsonIfExists(filePath) {
414
- try {
415
- const raw = await readFile(filePath, "utf8");
416
- return JSON.parse(raw);
417
- } catch (error) {
418
- if (
419
- error &&
420
- typeof error === "object" &&
421
- "code" in error &&
422
- error.code === "ENOENT"
423
- ) {
424
- return null;
425
- }
426
- throw error;
427
- }
428
- }
429
-
430
- async function acquireLock(lockPath, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
431
- const start = Date.now();
432
- await mkdir(path.dirname(lockPath), { recursive: true });
433
-
434
- while (true) {
435
- try {
436
- return await open(lockPath, "wx");
437
- } catch (error) {
438
- if (
439
- !(
440
- error &&
441
- typeof error === "object" &&
442
- "code" in error &&
443
- error.code === "EEXIST"
444
- )
445
- ) {
446
- throw error;
447
- }
448
- if (Date.now() - start >= timeoutMs) {
449
- throw new RuntimeInstallError(
450
- `timed out waiting for runtime install lock at ${lockPath}`,
451
- "install_lock_timeout",
452
- );
453
- }
454
- await sleep(LOCK_POLL_INTERVAL_MS);
455
- }
456
- }
457
- }
458
-
459
- async function withInstallLock(lockPath, callback) {
460
- const handle = await acquireLock(lockPath);
461
- try {
462
- return await callback();
463
- } finally {
464
- await handle.close().catch(() => {});
465
- await unlink(lockPath).catch(() => {});
466
- }
467
- }
468
-
469
- async function downloadArtifact(urlString, destinationPath) {
470
- const url = new URL(urlString);
471
- if (url.protocol === "file:") {
472
- await copyFile(fileURLToPath(url), destinationPath);
473
- return;
474
- }
475
- if (url.protocol !== "https:") {
476
- throw new RuntimeInstallError(
477
- `unsupported artifact URL protocol for ${urlString}`,
478
- "unsupported_artifact_protocol",
479
- );
480
- }
481
-
482
- const response = await fetch(url);
483
- if (!response.ok) {
484
- throw new RuntimeInstallError(
485
- `failed to download runtime artifact ${urlString}: ${response.status} ${response.statusText}`,
486
- "artifact_download_failed",
487
- );
488
- }
489
- const bytes = Buffer.from(await response.arrayBuffer());
490
- await writeFile(destinationPath, bytes);
491
- }
492
-
493
- function extractTarball(tarballPath, destinationDir) {
494
- execFileSync("tar", ["-xzf", tarballPath, "-C", destinationDir], {
495
- stdio: ["ignore", "pipe", "pipe"],
496
- });
497
- }
498
-
499
- function getBinPathForArtifact(rootDir, selectedArtifact, binName) {
500
- const binRelativePath = selectedArtifact?.bins?.[binName];
501
- if (typeof binRelativePath !== "string" || binRelativePath.length === 0) {
502
- throw new RuntimeInstallError(
503
- `runtime artifact does not provide a ${binName} bin`,
504
- "missing_runtime_bin",
505
- );
506
- }
507
- return path.join(rootDir, binRelativePath);
508
- }
509
-
510
- async function installationIsCurrent(installPaths, selectedArtifact) {
511
- try {
512
- const metadata = await readJsonIfExists(
513
- path.join(installPaths.releaseDir, "agenc-runtime-installation.json"),
514
- );
515
- if (!metadata) {
516
- return false;
517
- }
518
- if (
519
- metadata.sha256 !== selectedArtifact.sha256 ||
520
- metadata.url !== selectedArtifact.url ||
521
- metadata.runtimeVersion !== selectedArtifact.runtimeVersion
522
- ) {
523
- return false;
524
- }
525
- const agencBin = getBinPathForArtifact(
526
- installPaths.releaseDir,
527
- selectedArtifact,
528
- "agenc",
529
- );
530
- const runtimeBin = getBinPathForArtifact(
531
- installPaths.releaseDir,
532
- selectedArtifact,
533
- "agenc-runtime",
534
- );
535
- await stat(agencBin);
536
- await stat(runtimeBin);
537
- return true;
538
- } catch {
539
- return false;
540
- }
541
- }
542
-
543
- async function ensureCurrentPointer(installPaths) {
544
- await mkdir(path.dirname(installPaths.currentDir), { recursive: true });
545
- await rm(installPaths.currentDir, { recursive: true, force: true });
546
- await symlink(installPaths.releaseDir, installPaths.currentDir, "dir");
547
- }
548
-
549
- async function writeInstallState(statePath, payload) {
550
- await mkdir(path.dirname(statePath), { recursive: true });
551
- await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
552
- }
553
-
554
- function getStableManagedBinPaths(installPaths, selectedArtifact) {
555
- return {
556
- agenc: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "agenc"),
557
- "agenc-runtime": getBinPathForArtifact(
558
- installPaths.currentDir,
559
- selectedArtifact,
560
- "agenc-runtime",
561
- ),
562
- daemon: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "daemon"),
563
- watch: getBinPathForArtifact(installPaths.currentDir, selectedArtifact, "agenc-watch"),
564
- };
565
- }
566
-
567
- async function getDaemonPidInfo(homeDir) {
568
- const pidPath = path.join(getOperatorHome(homeDir), "daemon.pid");
569
- const pidInfo = await readJsonIfExists(pidPath);
570
- if (
571
- pidInfo &&
572
- typeof pidInfo.pid === "number" &&
573
- Number.isInteger(pidInfo.pid) &&
574
- pidInfo.pid > 0
575
- ) {
576
- try {
577
- process.kill(pidInfo.pid, 0);
578
- return pidInfo;
579
- } catch {
580
- return null;
581
- }
582
- }
583
- return null;
584
- }
585
-
586
- export async function ensureRuntimeInstalled(options = {}) {
587
- const packageRoot = options.packageRoot ?? getPackageRoot();
588
- const env = options.env ?? process.env;
589
- const homeDir = options.homeDir ?? os.homedir();
590
- const verifiedManifest = await loadVerifiedManifest({ packageRoot, env });
591
- if (!verifiedManifest) {
592
- throw new RuntimeInstallError(
593
- "no embedded or overridden runtime manifest is available; install a prepared agenc package or provide AGENC_RUNTIME_MANIFEST_FILE for local development",
594
- "missing_manifest",
595
- );
596
- }
597
-
598
- const selectedArtifact = selectArtifactEntry(verifiedManifest.manifest, {
599
- platform: options.platform,
600
- arch: options.arch,
601
- nodeVersion: options.nodeVersion,
602
- });
603
- const installPaths = getInstallPaths(selectedArtifact, homeDir);
604
- if (
605
- !options.force &&
606
- (await installationIsCurrent(installPaths, selectedArtifact))
607
- ) {
608
- await ensureCurrentPointer(installPaths);
609
- return {
610
- releaseDir: installPaths.releaseDir,
611
- currentDir: installPaths.currentDir,
612
- runtimeHome: installPaths.runtimeHome,
613
- statePath: installPaths.statePath,
614
- manifestSource: verifiedManifest.source.description,
615
- selectedArtifact,
616
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
617
- };
618
- }
619
-
620
- return withInstallLock(installPaths.lockPath, async () => {
621
- if (
622
- !options.force &&
623
- (await installationIsCurrent(installPaths, selectedArtifact))
624
- ) {
625
- await ensureCurrentPointer(installPaths);
626
- return {
627
- releaseDir: installPaths.releaseDir,
628
- currentDir: installPaths.currentDir,
629
- runtimeHome: installPaths.runtimeHome,
630
- statePath: installPaths.statePath,
631
- manifestSource: verifiedManifest.source.description,
632
- selectedArtifact,
633
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
634
- };
635
- }
636
-
637
- await mkdir(installPaths.tempRoot, { recursive: true });
638
- const tempDir = await mkdtemp(path.join(installPaths.tempRoot, "install-"));
639
- const tarballPath = path.join(tempDir, "runtime.tar.gz");
640
- const extractDir = path.join(tempDir, "extract");
641
- await mkdir(extractDir, { recursive: true });
642
-
643
- try {
644
- await downloadArtifact(selectedArtifact.url, tarballPath);
645
- const tarballBytes = await readFile(tarballPath);
646
- const actualSha = computeSha256(tarballBytes);
647
- if (actualSha !== selectedArtifact.sha256) {
648
- throw new RuntimeInstallError(
649
- `runtime artifact checksum mismatch for ${selectedArtifact.url}`,
650
- "artifact_checksum_mismatch",
651
- );
652
- }
653
-
654
- extractTarball(tarballPath, extractDir);
655
- const installMetadataPath = path.join(
656
- extractDir,
657
- "agenc-runtime-installation.json",
658
- );
659
- await writeFile(
660
- installMetadataPath,
661
- `${JSON.stringify(
662
- {
663
- installedAt: new Date().toISOString(),
664
- runtimeVersion: selectedArtifact.runtimeVersion,
665
- platform: selectedArtifact.platform,
666
- arch: selectedArtifact.arch,
667
- sha256: selectedArtifact.sha256,
668
- url: selectedArtifact.url,
669
- bins: selectedArtifact.bins,
670
- wrapperVersion: verifiedManifest.wrapperVersion,
671
- manifestDigest: verifiedManifest.manifestDigest,
672
- keyId: verifiedManifest.manifest.keyId,
673
- },
674
- null,
675
- 2,
676
- )}\n`,
677
- "utf8",
678
- );
679
-
680
- await mkdir(path.dirname(installPaths.releaseDir), { recursive: true });
681
- await rm(installPaths.releaseDir, { recursive: true, force: true });
682
- await rename(extractDir, installPaths.releaseDir);
683
- await ensureCurrentPointer(installPaths);
684
- await writeInstallState(installPaths.statePath, {
685
- installedAt: new Date().toISOString(),
686
- manifestSource: verifiedManifest.source.description,
687
- manifestDigest: verifiedManifest.manifestDigest,
688
- runtimeVersion: selectedArtifact.runtimeVersion,
689
- platform: selectedArtifact.platform,
690
- arch: selectedArtifact.arch,
691
- releaseDir: installPaths.releaseDir,
692
- currentDir: installPaths.currentDir,
693
- });
694
- } finally {
695
- await rm(tempDir, { recursive: true, force: true }).catch(() => {});
696
- }
697
-
698
- return {
699
- releaseDir: installPaths.releaseDir,
700
- currentDir: installPaths.currentDir,
701
- runtimeHome: installPaths.runtimeHome,
702
- statePath: installPaths.statePath,
703
- manifestSource: verifiedManifest.source.description,
704
- selectedArtifact,
705
- bins: getStableManagedBinPaths(installPaths, selectedArtifact),
706
- };
707
- });
708
- }
709
-
710
- export async function uninstallRuntime(options = {}) {
711
- const homeDir = options.homeDir ?? os.homedir();
712
- const force = options.force === true;
713
- const daemonPidInfo = await getDaemonPidInfo(homeDir);
714
- if (daemonPidInfo && !force) {
715
- throw new RuntimeInstallError(
716
- `refusing to uninstall while daemon ${daemonPidInfo.pid} is still running; stop AgenC first or rerun with --force`,
717
- "daemon_still_running",
718
- );
719
- }
720
-
721
- const packageRoot = options.packageRoot ?? getPackageRoot();
722
- const env = options.env ?? process.env;
723
- const verifiedManifest = await loadVerifiedManifest({ packageRoot, env }).catch(
724
- () => null,
725
- );
726
- const selectedArtifact = verifiedManifest
727
- ? selectArtifactEntry(verifiedManifest.manifest, {
728
- platform: options.platform,
729
- arch: options.arch,
730
- nodeVersion: options.nodeVersion,
731
- })
732
- : null;
733
-
734
- const runtimeHome = getWrapperRuntimeHome(homeDir);
735
- const statePath = path.join(runtimeHome, "install-state.json");
736
- const currentDir = path.join(runtimeHome, "current");
737
- const installDir = selectedArtifact
738
- ? getInstallPaths(selectedArtifact, homeDir).releaseDir
739
- : (await readJsonIfExists(statePath))?.releaseDir;
740
-
741
- if (!installDir) {
742
- return {
743
- removed: false,
744
- runtimeHome,
745
- releaseDir: null,
746
- preservedPaths: [getOperatorHome(homeDir)],
747
- };
748
- }
749
-
750
- await rm(installDir, { recursive: true, force: true });
751
- await rm(currentDir, { recursive: true, force: true });
752
- const state = await readJsonIfExists(statePath);
753
- if (state && state.releaseDir === installDir) {
754
- await rm(statePath, { force: true });
755
- }
756
- return {
757
- removed: true,
758
- runtimeHome,
759
- releaseDir: installDir,
760
- preservedPaths: [
761
- path.join(getOperatorHome(homeDir), "config.json"),
762
- path.join(getOperatorHome(homeDir), "daemon.pid"),
763
- path.join(getOperatorHome(homeDir), "replay-events.sqlite"),
764
- getOperatorHome(homeDir),
765
- ],
766
- };
767
- }
768
-
769
- export async function describeRuntimeInstall(options = {}) {
770
- const homeDir = options.homeDir ?? os.homedir();
771
- const packageRoot = options.packageRoot ?? getPackageRoot();
772
- const env = options.env ?? process.env;
773
- const runtimeHome = getWrapperRuntimeHome(homeDir);
774
- const statePath = path.join(runtimeHome, "install-state.json");
775
- const state = await readJsonIfExists(statePath);
776
- const manifest = await loadVerifiedManifest({ packageRoot, env }).catch(
777
- () => null,
778
- );
779
- const selectedArtifact = manifest
780
- ? selectArtifactEntry(manifest.manifest, {
781
- platform: options.platform,
782
- arch: options.arch,
783
- nodeVersion: options.nodeVersion,
784
- })
785
- : null;
786
-
787
- return {
788
- runtimeHome,
789
- currentDir: path.join(runtimeHome, "current"),
790
- statePath,
791
- installed: state !== null && typeof state.releaseDir === "string",
792
- releaseDir: state?.releaseDir ?? null,
793
- manifestSource: manifest?.source.description ?? null,
794
- manifestDigest: manifest?.manifestDigest ?? null,
795
- selectedArtifact,
796
- trustPolicy: manifest?.trustPolicy ?? null,
797
- };
798
- }
799
-
800
- async function spawnNodeScript(scriptPath, args, env, cwd) {
801
- return new Promise((resolve, reject) => {
802
- const child = spawn(process.execPath, [scriptPath, ...args], {
803
- stdio: "inherit",
804
- env,
805
- cwd,
806
- });
807
- child.on("error", reject);
808
- child.on("exit", (code, signal) => {
809
- if (signal) {
810
- resolve(1);
811
- return;
812
- }
813
- resolve(code ?? 0);
814
- });
815
- });
816
- }
817
-
818
- export async function spawnInstalledRuntimeBin(
819
- binName,
820
- args,
821
- options = {},
822
- ) {
823
- const runtime = await ensureRuntimeInstalled(options);
824
- const scriptPath = runtime.bins[binName];
825
- if (!scriptPath) {
826
- throw new RuntimeInstallError(
827
- `installed runtime does not expose ${binName}`,
828
- "missing_runtime_bin",
829
- );
830
- }
831
- const env = {
832
- ...(options.env ?? process.env),
833
- AGENC_WRAPPER_ACTIVE: "1",
834
- AGENC_WRAPPER_RUNTIME_HOME: runtime.runtimeHome,
835
- AGENC_WRAPPER_RELEASE_DIR: runtime.releaseDir,
836
- AGENC_WRAPPER_CURRENT_DIR: runtime.currentDir,
837
- AGENC_DAEMON_ENTRY: runtime.bins.daemon,
838
- AGENC_WATCH_ENTRY: runtime.bins.watch,
839
- };
840
- return spawnNodeScript(scriptPath, args, env, options.cwd);
841
- }
842
-
843
- export async function prefetchRuntimeOnInstall(options = {}) {
844
- const manifest = await loadVerifiedManifest(options);
845
- if (!manifest) {
846
- return { prefetched: false, reason: "missing_manifest" };
847
- }
848
- await ensureRuntimeInstalled({
849
- ...options,
850
- force: false,
851
- });
852
- return { prefetched: true };
853
- }
854
-
855
- // ---------------------------------------------------------------------------
856
- // Cut 6.1: --from-source dev mode
857
- //
858
- // The published-tarball install path lives above. This block adds a parallel
859
- // path that builds the runtime from a local source checkout and points
860
- // `~/.agenc/runtime/current` at it. Without this, the only way to land
861
- // changes on the running daemon is to publish a new tarball — which is
862
- // exactly the failure mode that left a 2-month silent-failure window in
863
- // place. The dev path is opt-in via `--from-source` and never runs by
864
- // default.
865
- // ---------------------------------------------------------------------------
866
-
867
- const FROM_SOURCE_PLATFORM_ARCH = "from-source";
868
-
869
- function resolveSourceRoot({
870
- env = process.env,
871
- cwd = process.cwd(),
872
- explicit,
873
- } = {}) {
874
- const candidates = [];
875
- if (typeof explicit === "string" && explicit.trim().length > 0) {
876
- candidates.push(path.resolve(explicit.trim()));
877
- }
878
- const envValue = env?.AGENC_RUNTIME_SOURCE_DIR;
879
- if (typeof envValue === "string" && envValue.trim().length > 0) {
880
- candidates.push(path.resolve(envValue.trim()));
881
- }
882
- // Walk upward from cwd looking for `runtime/package.json` whose name is
883
- // `@tetsuo-ai/runtime` — that's the agenc-core monorepo root.
884
- let current = path.resolve(cwd);
885
- for (let i = 0; i < 8; i++) {
886
- candidates.push(path.join(current, "runtime"));
887
- const parent = path.dirname(current);
888
- if (parent === current) break;
889
- current = parent;
890
- }
891
-
892
- for (const candidate of candidates) {
893
- try {
894
- const pkgPath = path.join(candidate, "package.json");
895
- const raw = readFileSync(pkgPath, "utf8");
896
- const parsed = JSON.parse(raw);
897
- if (parsed?.name === "@tetsuo-ai/runtime") {
898
- return candidate;
899
- }
900
- } catch {
901
- // try next
902
- }
903
- }
904
- return null;
905
- }
906
-
907
- async function readJsonSafe(filePath) {
908
- try {
909
- const raw = await readFile(filePath, "utf8");
910
- return JSON.parse(raw);
911
- } catch {
912
- return null;
913
- }
914
- }
915
-
916
- function runBuildScript(sourceRoot, env) {
917
- const result = spawn("npm", ["run", "build"], {
918
- cwd: sourceRoot,
919
- stdio: ["ignore", "inherit", "inherit"],
920
- env: { ...env },
921
- });
922
- return new Promise((resolvePromise, rejectPromise) => {
923
- result.on("error", rejectPromise);
924
- result.on("exit", (code) => {
925
- if (code === 0) {
926
- resolvePromise();
927
- } else {
928
- rejectPromise(
929
- new RuntimeInstallError(
930
- `from-source build failed with exit code ${code}`,
931
- "from_source_build_failed",
932
- ),
933
- );
934
- }
935
- });
936
- });
937
- }
938
-
939
- async function readBuildVersion(sourceRoot) {
940
- const versionPath = path.join(sourceRoot, "dist", "VERSION");
941
- return await readJsonSafe(versionPath);
942
- }
943
-
944
- function buildFromSourceArtifactDescriptor(sourceRoot, packageJson, versionFile) {
945
- const runtimeVersion =
946
- typeof packageJson?.version === "string" ? packageJson.version : "0.0.0";
947
- const distRoot = path.join(sourceRoot, "dist");
948
- const bins = {
949
- agenc: "bin/agenc.js",
950
- "agenc-runtime": "bin/agenc-runtime.js",
951
- daemon: "bin/daemon.js",
952
- "agenc-watch": "bin/agenc-watch.js",
953
- };
954
- return {
955
- runtimeVersion,
956
- platform: FROM_SOURCE_PLATFORM_ARCH,
957
- arch: "local",
958
- bins,
959
- sha256: versionFile?.commit ?? "from-source",
960
- distRoot,
961
- };
962
- }
963
-
964
- async function ensureFromSourceCurrentPointer({
965
- homeDir = os.homedir(),
966
- distRoot,
967
- }) {
968
- const runtimeHome = getWrapperRuntimeHome(homeDir);
969
- const releaseDir = path.join(
970
- runtimeHome,
971
- "releases",
972
- "from-source",
973
- FROM_SOURCE_PLATFORM_ARCH,
974
- );
975
- const currentDir = path.join(runtimeHome, "current");
976
- const statePath = path.join(runtimeHome, "install-state.json");
977
-
978
- await mkdir(path.dirname(releaseDir), { recursive: true });
979
- await removePathSafe(releaseDir);
980
- await mkdir(path.dirname(releaseDir), { recursive: true });
981
-
982
- // The release dir is a directory containing a single symlink (`dist`)
983
- // pointing back at the local source dist. The wrapper's bin paths look
984
- // for `<releaseDir>/bin/<binName>`, so we lay out the same shape using
985
- // a symlink for the entire `dist` tree.
986
- await symlink(distRoot, releaseDir, "dir");
987
-
988
- // Use removePathSafe (lstat-based) so we delete the SYMLINK at
989
- // `currentDir`, not the target it points at. The previous `rm
990
- // recursive` call followed the symlink and either failed silently
991
- // or removed the target dir contents while leaving the link intact,
992
- // causing the from-source flag to silently keep pointing at a
993
- // stale release.
994
- await removePathSafe(currentDir);
995
- await symlink(releaseDir, currentDir, "dir");
996
-
997
- await mkdir(path.dirname(statePath), { recursive: true });
998
- await writeFile(
999
- statePath,
1000
- `${JSON.stringify(
1001
- {
1002
- installedAt: new Date().toISOString(),
1003
- manifestSource: "from-source",
1004
- runtimeVersion: path.basename(distRoot),
1005
- platform: FROM_SOURCE_PLATFORM_ARCH,
1006
- arch: "local",
1007
- releaseDir,
1008
- currentDir,
1009
- },
1010
- null,
1011
- 2,
1012
- )}\n`,
1013
- "utf8",
1014
- );
1015
-
1016
- return { runtimeHome, releaseDir, currentDir, statePath };
1017
- }
1018
-
1019
- export async function ensureRuntimeFromSource(options = {}) {
1020
- const env = options.env ?? process.env;
1021
- const homeDir = options.homeDir ?? os.homedir();
1022
- const cwd = options.cwd ?? process.cwd();
1023
- const sourceRoot = resolveSourceRoot({
1024
- env,
1025
- cwd,
1026
- explicit: options.sourceDir,
1027
- });
1028
- if (!sourceRoot) {
1029
- throw new RuntimeInstallError(
1030
- "could not locate the agenc-core runtime source directory; pass --source-dir <path> or set AGENC_RUNTIME_SOURCE_DIR",
1031
- "from_source_root_not_found",
1032
- );
1033
- }
1034
- const distRoot = path.join(sourceRoot, "dist");
1035
- const skipBuild =
1036
- options.skipBuild === true || env?.AGENC_FROM_SOURCE_SKIP_BUILD === "1";
1037
-
1038
- if (!skipBuild) {
1039
- await runBuildScript(sourceRoot, env);
1040
- }
1041
- if (!existsSync(distRoot)) {
1042
- throw new RuntimeInstallError(
1043
- `from-source build did not produce ${distRoot}`,
1044
- "from_source_dist_missing",
1045
- );
1046
- }
1047
-
1048
- const packageJson = await readJsonSafe(path.join(sourceRoot, "package.json"));
1049
- const versionFile = await readBuildVersion(sourceRoot);
1050
- const selectedArtifact = buildFromSourceArtifactDescriptor(
1051
- sourceRoot,
1052
- packageJson,
1053
- versionFile,
1054
- );
1055
-
1056
- const installPaths = await ensureFromSourceCurrentPointer({
1057
- homeDir,
1058
- distRoot,
1059
- });
1060
-
1061
- return {
1062
- releaseDir: installPaths.releaseDir,
1063
- currentDir: installPaths.currentDir,
1064
- runtimeHome: installPaths.runtimeHome,
1065
- statePath: installPaths.statePath,
1066
- manifestSource: "from-source",
1067
- selectedArtifact,
1068
- fromSource: true,
1069
- sourceRoot,
1070
- versionFile,
1071
- bins: {
1072
- agenc: path.join(installPaths.currentDir, selectedArtifact.bins.agenc),
1073
- "agenc-runtime": path.join(
1074
- installPaths.currentDir,
1075
- selectedArtifact.bins["agenc-runtime"],
1076
- ),
1077
- daemon: path.join(installPaths.currentDir, selectedArtifact.bins.daemon),
1078
- watch: path.join(
1079
- installPaths.currentDir,
1080
- selectedArtifact.bins["agenc-watch"],
1081
- ),
1082
- },
1083
- };
1084
- }
1085
-
1086
- export async function spawnInstalledRuntimeBinFromSource(
1087
- binName,
1088
- args,
1089
- options = {},
1090
- ) {
1091
- const runtime = await ensureRuntimeFromSource(options);
1092
- const scriptPath = runtime.bins[binName];
1093
- if (!scriptPath) {
1094
- throw new RuntimeInstallError(
1095
- `from-source runtime does not expose ${binName}`,
1096
- "missing_runtime_bin",
1097
- );
1098
- }
1099
- const env = {
1100
- ...(options.env ?? process.env),
1101
- AGENC_WRAPPER_ACTIVE: "1",
1102
- AGENC_WRAPPER_RUNTIME_HOME: runtime.runtimeHome,
1103
- AGENC_WRAPPER_RELEASE_DIR: runtime.releaseDir,
1104
- AGENC_WRAPPER_CURRENT_DIR: runtime.currentDir,
1105
- AGENC_WRAPPER_FROM_SOURCE: "1",
1106
- AGENC_WRAPPER_SOURCE_ROOT: runtime.sourceRoot,
1107
- AGENC_DAEMON_ENTRY: runtime.bins.daemon,
1108
- AGENC_WATCH_ENTRY: runtime.bins.watch,
1109
- };
1110
- return spawnNodeScript(scriptPath, args, env, options.cwd);
1111
- }