@tetsuo-ai/agenc 0.10.0 → 0.13.0

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.
@@ -13,7 +13,14 @@ export interface RuntimeReleaseArtifact {
13
13
  readonly attestationUrl?: string;
14
14
  readonly attestationSha256?: string;
15
15
  readonly attestationBytes?: number;
16
- readonly bins?: { readonly agenc?: string };
16
+ readonly buildProvenanceUrl?: string;
17
+ readonly buildProvenanceSha256?: string;
18
+ readonly buildProvenanceBytes?: number;
19
+ readonly bins?: {
20
+ readonly agenc?: string;
21
+ readonly node?: string;
22
+ readonly nodeLibrary?: string;
23
+ };
17
24
  readonly [key: string]: unknown;
18
25
  }
19
26
 
@@ -32,6 +39,10 @@ export const MAX_RUNTIME_ARTIFACT_BYTES: number;
32
39
  export const MAX_RUNTIME_ATTESTATION_BYTES: number;
33
40
  export const OFFICIAL_RELEASE_REPOSITORY: string;
34
41
  export const OFFICIAL_SOURCE_REPOSITORY: string;
42
+ export const FROZEN_LEGACY_RUNTIME_VERSION: "0.7.2";
43
+ export const MINIMUM_PRIVATE_NODE_RUNTIME_VERSION: "0.11.2";
44
+ export const MINIMUM_DUAL_PROVENANCE_RUNTIME_VERSION: "0.13.0";
45
+ export const PRIVATE_NODE_BOOTSTRAP_RELEASE_TAG: "agenc-v0.11.2";
35
46
  export const OFFICIAL_RELEASE_WORKFLOW: string;
36
47
  export const RUNTIME_ATTESTATION_POLICY: Readonly<{
37
48
  repository: string;
@@ -40,6 +51,14 @@ export const RUNTIME_ATTESTATION_POLICY: Readonly<{
40
51
  oidcIssuer: string;
41
52
  predicateType: string;
42
53
  }>;
54
+ export const RUNTIME_BUILD_PROVENANCE_POLICY: Readonly<{
55
+ repository: string;
56
+ signerWorkflow: string;
57
+ hostname: string;
58
+ oidcIssuer: string;
59
+ predicateType: string;
60
+ sourceRef: "refs/heads/main";
61
+ }>;
43
62
  export const PINNED_GITHUB_CLI_VERSION: string;
44
63
  export const PINNED_GITHUB_CLI_ARTIFACTS: Readonly<Record<string, Readonly<{
45
64
  file: string;
@@ -47,6 +66,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS: Readonly<Record<string, Readonly<{
47
66
  sha256: string;
48
67
  bytes: number;
49
68
  executable: string;
69
+ executableSha256: string;
70
+ executableBytes: number;
50
71
  }>>>;
51
72
  export const RUNTIME_MANIFEST_TRUST_MODES: readonly RuntimeManifestTrustMode[];
52
73
 
@@ -57,6 +78,13 @@ export function canonicalRuntimeAttestationVerificationArgs(options: {
57
78
  readonly sourceRef: string;
58
79
  }): readonly string[];
59
80
 
81
+ export function canonicalRuntimeBuildProvenanceVerificationArgs(options: {
82
+ readonly subjectPath: string;
83
+ readonly bundlePath: string;
84
+ readonly sourceCommit: string;
85
+ readonly sourceRef?: "refs/heads/main";
86
+ }): readonly string[];
87
+
60
88
  export type LocalFileUrlPlatform =
61
89
  | "win"
62
90
  | "win32"
@@ -79,6 +107,32 @@ export function canonicalRuntimeArtifactName(
79
107
  >,
80
108
  ): string;
81
109
 
110
+ export function canonicalRuntimeNodeBin(platform: string): string;
111
+
112
+ export function canonicalRuntimeNodeLibrary(platform: string): string | undefined;
113
+
114
+ export function requireSupportedRuntimeVersion(version: string): string;
115
+
116
+ export function runtimeVersionRequiresDualProvenance(version: string): boolean;
117
+
118
+ export interface RuntimeReleaseCandidateIdentity {
119
+ readonly workflow: "release-runtime.yml";
120
+ readonly runId: number;
121
+ readonly runAttempt: number;
122
+ readonly runUrl: string;
123
+ readonly phase: "candidate";
124
+ readonly sourceRef: "refs/heads/main";
125
+ readonly evidenceSha256: string;
126
+ }
127
+
128
+ export function validateRuntimeReleaseCandidateIdentity<
129
+ T extends RuntimeReleaseCandidateIdentity,
130
+ >(
131
+ value: T | undefined,
132
+ sourceCommit: unknown,
133
+ options?: { readonly required?: boolean },
134
+ ): T | undefined;
135
+
82
136
  export function canonicalLocalFileUrlToPath(
83
137
  value: string,
84
138
  platform?: LocalFileUrlPlatform,
@@ -10,6 +10,11 @@ export const MAX_RUNTIME_ARTIFACT_BYTES = 256 * 1024 * 1024;
10
10
  export const MAX_RUNTIME_ATTESTATION_BYTES = 4 * 1024 * 1024;
11
11
  export const OFFICIAL_RELEASE_REPOSITORY = "tetsuo-ai/agenc-releases";
12
12
  export const OFFICIAL_SOURCE_REPOSITORY = "tetsuo-ai/agenc-core";
13
+ export const FROZEN_LEGACY_RUNTIME_VERSION = "0.7.2";
14
+ export const MINIMUM_PRIVATE_NODE_RUNTIME_VERSION = "0.11.2";
15
+ export const MINIMUM_DUAL_PROVENANCE_RUNTIME_VERSION = "0.13.0";
16
+ export const PRIVATE_NODE_BOOTSTRAP_RELEASE_TAG =
17
+ `agenc-v${MINIMUM_PRIVATE_NODE_RUNTIME_VERSION}`;
13
18
  export const OFFICIAL_RELEASE_WORKFLOW =
14
19
  "tetsuo-ai/agenc-core/.github/workflows/release-runtime.yml";
15
20
  export const RUNTIME_ATTESTATION_POLICY = Object.freeze({
@@ -19,6 +24,10 @@ export const RUNTIME_ATTESTATION_POLICY = Object.freeze({
19
24
  oidcIssuer: "https://token.actions.githubusercontent.com",
20
25
  predicateType: "https://slsa.dev/provenance/v1",
21
26
  });
27
+ export const RUNTIME_BUILD_PROVENANCE_POLICY = Object.freeze({
28
+ ...RUNTIME_ATTESTATION_POLICY,
29
+ sourceRef: "refs/heads/main",
30
+ });
22
31
  export const PINNED_GITHUB_CLI_VERSION = "2.96.0";
23
32
  export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
24
33
  "linux-x64": Object.freeze({
@@ -27,6 +36,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
27
36
  sha256: "83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60",
28
37
  bytes: 14652560,
29
38
  executable: "gh_2.96.0_linux_amd64/bin/gh",
39
+ executableSha256: "56b8bbbb27b066ecb33dbef9a256dc9d1314adaeff0908a752feba6c34053b40",
40
+ executableBytes: 40722594,
30
41
  }),
31
42
  "linux-arm64": Object.freeze({
32
43
  file: "gh_2.96.0_linux_arm64.tar.gz",
@@ -34,6 +45,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
34
45
  sha256: "06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909",
35
46
  bytes: 13321232,
36
47
  executable: "gh_2.96.0_linux_arm64/bin/gh",
48
+ executableSha256: "f903d2fa04ae78ee8f8df0186364d0b92f078d2720053941a47e0a3beaef54f0",
49
+ executableBytes: 37879970,
37
50
  }),
38
51
  "darwin-x64": Object.freeze({
39
52
  file: "gh_2.96.0_macOS_amd64.zip",
@@ -41,6 +54,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
41
54
  sha256: "4bd449df9ad639391bc62b8032546f0fe9edcd8526e06682a4f88abd8c5d163c",
42
55
  bytes: 15298430,
43
56
  executable: "gh_2.96.0_macOS_amd64/bin/gh",
57
+ executableSha256: "49380b19e758c14ce5509afe0ced58777fe45cfbcd5989941f7314d5f7e468c8",
58
+ executableBytes: 41773632,
44
59
  }),
45
60
  "darwin-arm64": Object.freeze({
46
61
  file: "gh_2.96.0_macOS_arm64.zip",
@@ -48,6 +63,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
48
63
  sha256: "f23a0c37d963aacc3bed703ccbd59b41c5ca22101fab7f00eb2b7cad23aba463",
49
64
  bytes: 13950131,
50
65
  executable: "gh_2.96.0_macOS_arm64/bin/gh",
66
+ executableSha256: "b1d6c442fde99ca27c04e1e74d624895abe37785f4a3e9e9b684bf7586ce4bc8",
67
+ executableBytes: 38817216,
51
68
  }),
52
69
  "win-x64": Object.freeze({
53
70
  file: "gh_2.96.0_windows_amd64.zip",
@@ -55,6 +72,8 @@ export const PINNED_GITHUB_CLI_ARTIFACTS = Object.freeze({
55
72
  sha256: "c2d6acc935cd2f00e2144d7e036d5cd82e6b6bd5594e8c75aa75ef2a4ed6aac3",
56
73
  bytes: 14821821,
57
74
  executable: "gh_2.96.0_windows_amd64/bin/gh.exe",
75
+ executableSha256: "cd79f16203f1fbe56937c4c96e2b6eadd10549418dcb241d91576ac77af0ac8b",
76
+ executableBytes: 41504056,
58
77
  }),
59
78
  });
60
79
  export const RUNTIME_MANIFEST_TRUST_MODES = Object.freeze([
@@ -65,17 +84,19 @@ export const RUNTIME_MANIFEST_TRUST_MODES = Object.freeze([
65
84
 
66
85
  const TRUST_MODES = new Set(RUNTIME_MANIFEST_TRUST_MODES);
67
86
 
68
- export function canonicalRuntimeAttestationVerificationArgs({
87
+ function canonicalRuntimeProvenanceVerificationArgs({
69
88
  subjectPath,
70
89
  bundlePath,
71
90
  sourceCommit,
72
91
  sourceRef,
92
+ sourceRefPattern,
93
+ sourceRefError,
73
94
  }) {
74
95
  if (!/^[0-9a-f]{40,64}$/.test(sourceCommit ?? "")) {
75
96
  throw new Error("runtime attestation policy requires an exact source commit");
76
97
  }
77
- if (typeof sourceRef !== "string" || !sourceRef.startsWith("refs/tags/agenc-v")) {
78
- throw new Error("runtime attestation policy requires the canonical release source ref");
98
+ if (typeof sourceRef !== "string" || !sourceRefPattern.test(sourceRef)) {
99
+ throw new Error(sourceRefError);
79
100
  }
80
101
  return [
81
102
  "attestation",
@@ -103,6 +124,40 @@ export function canonicalRuntimeAttestationVerificationArgs({
103
124
  ];
104
125
  }
105
126
 
127
+ export function canonicalRuntimeAttestationVerificationArgs({
128
+ subjectPath,
129
+ bundlePath,
130
+ sourceCommit,
131
+ sourceRef,
132
+ }) {
133
+ return canonicalRuntimeProvenanceVerificationArgs({
134
+ subjectPath,
135
+ bundlePath,
136
+ sourceCommit,
137
+ sourceRef,
138
+ sourceRefPattern: /^refs\/tags\/agenc-v[0-9]+\.[0-9]+\.[0-9]+$/,
139
+ sourceRefError:
140
+ "runtime attestation policy requires the canonical release source ref",
141
+ });
142
+ }
143
+
144
+ export function canonicalRuntimeBuildProvenanceVerificationArgs({
145
+ subjectPath,
146
+ bundlePath,
147
+ sourceCommit,
148
+ sourceRef = RUNTIME_BUILD_PROVENANCE_POLICY.sourceRef,
149
+ }) {
150
+ return canonicalRuntimeProvenanceVerificationArgs({
151
+ subjectPath,
152
+ bundlePath,
153
+ sourceCommit,
154
+ sourceRef,
155
+ sourceRefPattern: /^refs\/heads\/main$/,
156
+ sourceRefError:
157
+ "runtime build provenance policy requires the canonical main source ref",
158
+ });
159
+ }
160
+
106
161
  export function requireRuntimeManifestTrustMode(trustMode) {
107
162
  if (!TRUST_MODES.has(trustMode)) {
108
163
  throw new Error(`agenc: unsupported runtime manifest trust mode ${String(trustMode)}`);
@@ -115,6 +170,100 @@ export function canonicalRuntimeArtifactName(manifest, artifact) {
115
170
  `-node${artifact.nodeMajor}-abi${artifact.nodeModuleAbi}.tar.gz`;
116
171
  }
117
172
 
173
+ export function canonicalRuntimeNodeBin(platform) {
174
+ if (platform === "win" || platform === "win32") {
175
+ return "node_modules/.agenc-node/node.exe";
176
+ }
177
+ if (platform === "linux" || platform === "darwin") {
178
+ return "node_modules/.agenc-node/bin/node";
179
+ }
180
+ throw new Error(`agenc: unsupported runtime Node platform ${String(platform)}`);
181
+ }
182
+
183
+ export function canonicalRuntimeNodeLibrary(platform) {
184
+ if (platform === "linux") {
185
+ return "node_modules/.agenc-node/lib";
186
+ }
187
+ if (platform === "darwin" || platform === "win" || platform === "win32") {
188
+ return undefined;
189
+ }
190
+ throw new Error(`agenc: unsupported runtime Node platform ${String(platform)}`);
191
+ }
192
+
193
+ export function requireSupportedRuntimeVersion(version) {
194
+ if (version === FROZEN_LEGACY_RUNTIME_VERSION) {
195
+ return version;
196
+ }
197
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(version ?? "");
198
+ if (match === null) {
199
+ throw new Error("agenc: runtime version is not a canonical semantic version");
200
+ }
201
+ const actual = match.slice(1, 4).map(Number);
202
+ const minimum = MINIMUM_PRIVATE_NODE_RUNTIME_VERSION.split(".").map(Number);
203
+ for (let index = 0; index < minimum.length; index += 1) {
204
+ if (actual[index] > minimum[index]) return version;
205
+ if (actual[index] < minimum[index]) {
206
+ throw new Error(
207
+ `agenc: runtime ${version} has no supported standalone activation contract; ` +
208
+ `use the frozen ${FROZEN_LEGACY_RUNTIME_VERSION} bridge with host Node 25.9, ` +
209
+ `or ${MINIMUM_PRIVATE_NODE_RUNTIME_VERSION} and newer with private Node`,
210
+ );
211
+ }
212
+ }
213
+ return version;
214
+ }
215
+
216
+ export function runtimeVersionRequiresDualProvenance(version) {
217
+ requireSupportedRuntimeVersion(version);
218
+ if (version === FROZEN_LEGACY_RUNTIME_VERSION) return false;
219
+ const actual = version.split("-", 1)[0].split(".").map(Number);
220
+ const minimum = MINIMUM_DUAL_PROVENANCE_RUNTIME_VERSION.split(".").map(Number);
221
+ for (let index = 0; index < minimum.length; index += 1) {
222
+ if (actual[index] > minimum[index]) return true;
223
+ if (actual[index] < minimum[index]) return false;
224
+ }
225
+ return true;
226
+ }
227
+
228
+ const RUNTIME_RELEASE_CANDIDATE_KEYS = Object.freeze([
229
+ "workflow",
230
+ "runId",
231
+ "runAttempt",
232
+ "runUrl",
233
+ "phase",
234
+ "sourceRef",
235
+ "evidenceSha256",
236
+ ]);
237
+
238
+ export function validateRuntimeReleaseCandidateIdentity(
239
+ value,
240
+ sourceCommit,
241
+ { required = false } = {},
242
+ ) {
243
+ if (value === undefined && !required) return undefined;
244
+ if (
245
+ value === null ||
246
+ typeof value !== "object" ||
247
+ Array.isArray(value) ||
248
+ Object.keys(value).sort().join("\0") !==
249
+ [...RUNTIME_RELEASE_CANDIDATE_KEYS].sort().join("\0") ||
250
+ value.workflow !== "release-runtime.yml" ||
251
+ !Number.isSafeInteger(value.runId) ||
252
+ value.runId <= 0 ||
253
+ !Number.isSafeInteger(value.runAttempt) ||
254
+ value.runAttempt <= 0 ||
255
+ value.runUrl !==
256
+ `https://github.com/tetsuo-ai/agenc-core/actions/runs/${value.runId}` ||
257
+ value.phase !== "candidate" ||
258
+ value.sourceRef !== RUNTIME_BUILD_PROVENANCE_POLICY.sourceRef ||
259
+ !/^[0-9a-f]{64}$/.test(value.evidenceSha256 ?? "") ||
260
+ !/^[0-9a-f]{40}$/.test(sourceCommit ?? "")
261
+ ) {
262
+ throw new Error("agenc: runtime release candidate identity is invalid");
263
+ }
264
+ return value;
265
+ }
266
+
118
267
  function localFileUrlUsesWindowsPaths(platform) {
119
268
  if (platform === "win" || platform === "win32") return true;
120
269
  if (
@@ -234,7 +383,10 @@ function validateArtifactUrl(manifest, artifact, trustMode) {
234
383
  if (
235
384
  artifact.attestationUrl !== undefined ||
236
385
  artifact.attestationSha256 !== undefined ||
237
- artifact.attestationBytes !== undefined
386
+ artifact.attestationBytes !== undefined ||
387
+ artifact.buildProvenanceUrl !== undefined ||
388
+ artifact.buildProvenanceSha256 !== undefined ||
389
+ artifact.buildProvenanceBytes !== undefined
238
390
  ) {
239
391
  throw new Error("agenc: explicit local runtime artifacts must not declare remote attestations");
240
392
  }
@@ -268,6 +420,31 @@ function validateArtifactUrl(manifest, artifact, trustMode) {
268
420
  throw new Error("agenc: runtime artifact attestation size is invalid");
269
421
  }
270
422
  }
423
+ const hasBuildProvenance =
424
+ artifact.buildProvenanceUrl !== undefined ||
425
+ artifact.buildProvenanceSha256 !== undefined ||
426
+ artifact.buildProvenanceBytes !== undefined;
427
+ if (
428
+ (
429
+ trustMode === "official" &&
430
+ runtimeVersionRequiresDualProvenance(manifest.runtimeVersion)
431
+ ) ||
432
+ hasBuildProvenance
433
+ ) {
434
+ if (artifact.buildProvenanceUrl !== `${artifact.url}.build.sigstore.json`) {
435
+ throw new Error("agenc: runtime artifact build provenance URL is not canonical");
436
+ }
437
+ if (!/^[0-9a-f]{64}$/.test(artifact.buildProvenanceSha256 ?? "")) {
438
+ throw new Error("agenc: runtime artifact build provenance digest is invalid");
439
+ }
440
+ if (
441
+ !Number.isSafeInteger(artifact.buildProvenanceBytes) ||
442
+ artifact.buildProvenanceBytes <= 0 ||
443
+ artifact.buildProvenanceBytes > MAX_RUNTIME_ATTESTATION_BYTES
444
+ ) {
445
+ throw new Error("agenc: runtime artifact build provenance size is invalid");
446
+ }
447
+ }
271
448
  }
272
449
 
273
450
  export function validateRuntimeReleaseManifest(
@@ -289,6 +466,7 @@ export function validateRuntimeReleaseManifest(
289
466
  if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(manifest.runtimeVersion ?? "")) {
290
467
  throw new Error("agenc: runtime manifest has an invalid runtimeVersion");
291
468
  }
469
+ requireSupportedRuntimeVersion(manifest.runtimeVersion);
292
470
  if (manifest.releaseTag !== `agenc-v${manifest.runtimeVersion}`) {
293
471
  throw new Error("agenc: runtime manifest releaseTag does not match runtimeVersion");
294
472
  }
@@ -345,7 +523,14 @@ export function validateRuntimeReleaseManifest(
345
523
  !Number.isSafeInteger(artifact.bytes) ||
346
524
  artifact.bytes <= 0 ||
347
525
  artifact.bytes > MAX_RUNTIME_ARTIFACT_BYTES ||
348
- artifact.bins?.agenc !== "node_modules/@tetsuo-ai/runtime/bin/agenc"
526
+ artifact.bins?.agenc !== "node_modules/@tetsuo-ai/runtime/bin/agenc" ||
527
+ (
528
+ manifest.runtimeVersion !== FROZEN_LEGACY_RUNTIME_VERSION &&
529
+ (
530
+ artifact.bins?.node !== canonicalRuntimeNodeBin(artifact.platform) ||
531
+ artifact.bins?.nodeLibrary !== canonicalRuntimeNodeLibrary(artifact.platform)
532
+ )
533
+ )
349
534
  ) {
350
535
  throw new Error(`agenc: runtime manifest artifact identity is invalid (${identity})`);
351
536
  }
@@ -372,6 +557,15 @@ export function validateRuntimeReleaseManifest(
372
557
  ) {
373
558
  throw new Error("agenc: runtime manifest build provenance is invalid");
374
559
  }
560
+ validateRuntimeReleaseCandidateIdentity(
561
+ build.releaseCandidate,
562
+ build.sourceCommit,
563
+ {
564
+ required:
565
+ trustMode === "official" &&
566
+ runtimeVersionRequiresDualProvenance(manifest.runtimeVersion),
567
+ },
568
+ );
375
569
  for (const artifact of manifest.artifacts) {
376
570
  if (
377
571
  artifact.nodeMajor !== build.nodeMajor ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tetsuo-ai/agenc",
3
- "version": "0.10.0",
3
+ "version": "0.13.0",
4
4
  "description": "Public AgenC CLI launcher package",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,7 @@
23
23
  "lib/activation-lock-identity.mjs",
24
24
  "lib/generated-wrapper.d.mts",
25
25
  "lib/generated-wrapper.mjs",
26
+ "lib/runtime-archive.d.mts",
26
27
  "lib/runtime-archive.mjs",
27
28
  "lib/runtime-compatibility.mjs",
28
29
  "lib/runtime-manager.mjs",
@@ -47,6 +48,6 @@
47
48
  "@tetsuo-ai/runtime": "file:../../runtime"
48
49
  },
49
50
  "engines": {
50
- "node": ">=25.9.0 <26.0.0"
51
+ "node": ">=26.5.0 <27.0.0"
51
52
  }
52
53
  }
package/src/launcher.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
4
5
  import { homedir } from "node:os";
5
- import { dirname, join, resolve } from "node:path";
6
+ import { delimiter, dirname, join, resolve } from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
8
9
  const DEFAULT_READY_TIMEOUT_MS = 2000;
@@ -74,11 +75,17 @@ export function isDaemonCommand(argv) {
74
75
  export function resolveRuntimeBin(requireFn = requireFromLauncher) {
75
76
  try {
76
77
  const runtimeEntry = requireFn.resolve("@tetsuo-ai/runtime");
78
+ const runtimeRoot = realpathSync(resolve(dirname(runtimeEntry), ".."));
79
+ const repositoryRuntimeRoot = realpathSync(
80
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "runtime"),
81
+ );
82
+ if (runtimeRoot !== repositoryRuntimeRoot) return null;
77
83
  return resolve(dirname(runtimeEntry), "../bin/agenc");
78
84
  } catch (error) {
79
85
  if (
80
86
  error?.code === "MODULE_NOT_FOUND" ||
81
- error?.code === "ERR_MODULE_NOT_FOUND"
87
+ error?.code === "ERR_MODULE_NOT_FOUND" ||
88
+ error?.code === "ENOENT"
82
89
  ) {
83
90
  return null;
84
91
  }
@@ -86,8 +93,41 @@ export function resolveRuntimeBin(requireFn = requireFromLauncher) {
86
93
  }
87
94
  }
88
95
 
89
- // Resolve the runtime entry for either mode: prefer the dev file:-link, else
90
- // ensure (download + verify + extract) the published per-platform runtime.
96
+ function hostRuntimeLaunch(runtimeBin) {
97
+ return { runtimeBin, nodeBin: process.execPath };
98
+ }
99
+
100
+ function requireRuntimeLaunch(value) {
101
+ if (
102
+ value === null ||
103
+ typeof value !== "object" ||
104
+ typeof value.runtimeBin !== "string" ||
105
+ typeof value.nodeBin !== "string" ||
106
+ (
107
+ value.nodeLibraryPath !== undefined &&
108
+ typeof value.nodeLibraryPath !== "string"
109
+ )
110
+ ) {
111
+ throw new Error("agenc: runtime manager returned an invalid launch contract");
112
+ }
113
+ return value;
114
+ }
115
+
116
+ // Resolve every path needed to execute the runtime. Development file: links
117
+ // intentionally use the current Node process; published artifacts always
118
+ // return their embedded Node (and the Linux compatibility-library directory).
119
+ export async function resolveRuntimeLaunchAsync({
120
+ requireFn = requireFromLauncher,
121
+ ensureFn,
122
+ } = {}) {
123
+ const dev = resolveRuntimeBin(requireFn);
124
+ if (dev !== null) return hostRuntimeLaunch(dev);
125
+ const ensureRuntimeLaunch =
126
+ ensureFn ?? (await import("../lib/runtime-manager.mjs")).ensureRuntimeLaunch;
127
+ return requireRuntimeLaunch(await ensureRuntimeLaunch());
128
+ }
129
+
130
+ // Retained for callers that only need the script path.
91
131
  export async function resolveRuntimeBinAsync({
92
132
  requireFn = requireFromLauncher,
93
133
  ensureFn,
@@ -96,7 +136,10 @@ export async function resolveRuntimeBinAsync({
96
136
  if (dev !== null) return dev;
97
137
  const ensureRuntime =
98
138
  ensureFn ?? (await import("../lib/runtime-manager.mjs")).ensureRuntime;
99
- return ensureRuntime();
139
+ const ensured = await ensureRuntime();
140
+ return typeof ensured === "string"
141
+ ? ensured
142
+ : requireRuntimeLaunch(ensured).runtimeBin;
100
143
  }
101
144
 
102
145
  export async function spawnNodeScript(
@@ -107,12 +150,27 @@ export async function spawnNodeScript(
107
150
  cwd = process.cwd(),
108
151
  stdio = "inherit",
109
152
  spawnFn = spawn,
153
+ nodeBin = process.execPath,
154
+ nodeLibraryPath,
110
155
  } = {},
111
156
  ) {
157
+ const childEnv = { ...env };
158
+ const pathKey =
159
+ Object.keys(childEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
160
+ const nodeDir = dirname(nodeBin);
161
+ childEnv[pathKey] = childEnv[pathKey]
162
+ ? `${nodeDir}${delimiter}${childEnv[pathKey]}`
163
+ : nodeDir;
164
+ if (nodeLibraryPath !== undefined) {
165
+ // The embedded Linux Node requires the artifact's pinned libatomic. Do not
166
+ // search an operator-controlled ambient library path before that exact
167
+ // directory.
168
+ childEnv.LD_LIBRARY_PATH = nodeLibraryPath;
169
+ }
112
170
  return new Promise((resolveExit, reject) => {
113
- const child = spawnFn(process.execPath, [scriptPath, ...args], {
171
+ const child = spawnFn(nodeBin, [scriptPath, ...args], {
114
172
  cwd,
115
- env,
173
+ env: childEnv,
116
174
  stdio,
117
175
  });
118
176
  child.once("error", reject);
@@ -174,6 +232,8 @@ export async function ensureDaemonForLaunch({
174
232
  env = process.env,
175
233
  cwd = process.cwd(),
176
234
  runtimeBin = resolveRuntimeBin(),
235
+ runtimeNodeBin = process.execPath,
236
+ runtimeNodeLibraryPath,
177
237
  userHome = homedir(),
178
238
  readText = readFile,
179
239
  signalPid = process.kill,
@@ -197,7 +257,12 @@ export async function ensureDaemonForLaunch({
197
257
  return { status: "already-running" };
198
258
  }
199
259
 
200
- await spawnDaemonFn(runtimeBin, { env, cwd });
260
+ await spawnDaemonFn(runtimeBin, {
261
+ env,
262
+ cwd,
263
+ nodeBin: runtimeNodeBin,
264
+ nodeLibraryPath: runtimeNodeLibraryPath,
265
+ });
201
266
  const ready = await waitForReadyFn({ env, userHome, readText, signalPid });
202
267
  if (!ready) {
203
268
  throw new Error("AgenC daemon did not become ready before timeout");
@@ -210,16 +275,29 @@ export async function main(
210
275
  {
211
276
  env = process.env,
212
277
  cwd = process.cwd(),
278
+ runtimeLaunch,
213
279
  runtimeBin,
280
+ runtimeNodeBin = process.execPath,
281
+ runtimeNodeLibraryPath,
214
282
  userHome = homedir(),
215
283
  } = {},
216
284
  ) {
217
285
  // Resolve (and, in a published install, download + verify) the runtime before
218
286
  // anything tries to spawn it. Sync default is avoided: it returns null when
219
287
  // runtime isn't an npm dep, which is the normal published case.
220
- let resolvedBin = runtimeBin;
288
+ let resolvedLaunch = runtimeLaunch ??
289
+ (runtimeBin === undefined
290
+ ? undefined
291
+ : {
292
+ runtimeBin,
293
+ nodeBin: runtimeNodeBin,
294
+ ...(runtimeNodeLibraryPath === undefined
295
+ ? {}
296
+ : { nodeLibraryPath: runtimeNodeLibraryPath }),
297
+ });
221
298
  try {
222
- resolvedBin ??= await resolveRuntimeBinAsync();
299
+ resolvedLaunch ??= await resolveRuntimeLaunchAsync();
300
+ resolvedLaunch = requireRuntimeLaunch(resolvedLaunch);
223
301
  } catch (error) {
224
302
  process.stderr.write(
225
303
  `agenc: could not obtain runtime: ${
@@ -229,7 +307,15 @@ export async function main(
229
307
  return 1;
230
308
  }
231
309
  try {
232
- await ensureDaemonForLaunch({ argv, env, cwd, runtimeBin: resolvedBin, userHome });
310
+ await ensureDaemonForLaunch({
311
+ argv,
312
+ env,
313
+ cwd,
314
+ runtimeBin: resolvedLaunch.runtimeBin,
315
+ runtimeNodeBin: resolvedLaunch.nodeBin,
316
+ runtimeNodeLibraryPath: resolvedLaunch.nodeLibraryPath,
317
+ userHome,
318
+ });
233
319
  } catch (error) {
234
320
  // The launcher is transport, not policy: a daemon that cannot start must
235
321
  // not block daemon-independent commands. `agenc update` is the canonical
@@ -245,7 +331,13 @@ export async function main(
245
331
  "agenc: continuing without the daemon; commands that require it will report the failure\n",
246
332
  );
247
333
  }
248
- return spawnNodeScript(resolvedBin, argv, { env, cwd, stdio: "inherit" });
334
+ return spawnNodeScript(resolvedLaunch.runtimeBin, argv, {
335
+ env,
336
+ cwd,
337
+ stdio: "inherit",
338
+ nodeBin: resolvedLaunch.nodeBin,
339
+ nodeLibraryPath: resolvedLaunch.nodeLibraryPath,
340
+ });
249
341
  }
250
342
 
251
343
  if (process.argv[1] === fileURLToPath(import.meta.url)) {