@mithran-hq/aegis 0.9.14 → 0.9.16

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.
Files changed (3) hide show
  1. package/README.md +8 -6
  2. package/aegis.mjs +287 -50
  3. package/package.json +4 -9
package/README.md CHANGED
@@ -2,16 +2,18 @@
2
2
 
3
3
  Aegis is the Mithran CLI for account authentication, agent gateway
4
4
  configuration, and Forge deploys. This package is the npm launcher; it runs the
5
- native `aegis` binary from the matching `@mithran-hq/aegis-darwin-arm64` package.
5
+ native `aegis` binary from the platform package selected at install time:
6
+ `@mithran-hq/aegis-darwin-arm64` on macOS 15+ Apple silicon and
7
+ `@mithran-hq/aegis-linux-x64` on Linux x64.
6
8
 
7
9
  ## Requirements
8
10
 
9
- - macOS 15 or later
10
- - Apple silicon (arm64)
11
+ - macOS 15 or later on Apple silicon (arm64), or Linux x86-64 with glibc
11
12
  - Node.js and npm
12
13
 
13
- The launcher refuses unsupported platforms and macOS versions before it starts
14
- the native binary.
14
+ The launcher refuses unsupported platforms, unsupported macOS versions and
15
+ Linux hosts older than the platform package's glibc floor before it starts the
16
+ native binary.
15
17
 
16
18
  ## Install
17
19
 
@@ -27,7 +29,7 @@ candidate and `latest` still names the previous accepted build. Install a pinned
27
29
  version when you need a fixed build:
28
30
 
29
31
  ```bash
30
- npm install -g @mithran-hq/aegis@0.9.14
32
+ npm install -g @mithran-hq/aegis@0.9.16
31
33
  ```
32
34
 
33
35
  Check the installed version:
package/aegis.mjs CHANGED
@@ -1,6 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from "node:crypto";
3
- import { existsSync, readFileSync, statSync, writeSync } from "node:fs";
3
+ import {
4
+ closeSync,
5
+ constants,
6
+ existsSync,
7
+ fchmodSync,
8
+ fstatSync,
9
+ lstatSync,
10
+ openSync,
11
+ readFileSync,
12
+ statSync,
13
+ writeSync,
14
+ } from "node:fs";
4
15
  import { createRequire } from "node:module";
5
16
  import { dirname, join } from "node:path";
6
17
  import { fileURLToPath } from "node:url";
@@ -14,26 +25,50 @@ const MANIFEST_NAME = "aegis.manifest.json";
14
25
  const MINIMUM_MACOS_MAJOR = 15;
15
26
  const MH_MAGIC_64 = 0xfeedfacf;
16
27
  const CPU_TYPE_ARM64 = 0x0100000c;
28
+ const ELF_CLASS_64 = 2;
29
+ const ELF_DATA_LSB = 1;
30
+ const EM_X86_64 = 62;
31
+ // The platform packages the launcher can select. The native package must be
32
+ // installed as an optional dependency at the launcher's own version.
33
+ const SUPPORTED = {
34
+ "darwin-arm64": { stem: "aegis-darwin-arm64", kind: "mach-o-arm64" },
35
+ "linux-x64": { stem: "aegis-linux-x64", kind: "elf-x86_64" },
36
+ };
37
+ const SUPPORTED_HOSTS = Object.keys(SUPPORTED).join(", ");
17
38
  const FORWARDED_SIGNALS = ["SIGHUP", "SIGINT", "SIGTERM", "SIGQUIT"];
18
39
  // The conventional 128+signal exit status for a payload that died by a signal.
19
40
  // The launcher must never re-deliver that signal to itself: a second core-dump
20
41
  // signal from the wrapper is a crash surface of its own and can take the
21
42
  // foreground job, and the shell's job control, down with it.
22
43
  const SIGNAL_NUMBERS = {
23
- SIGHUP: 1,
24
- SIGINT: 2,
25
- SIGQUIT: 3,
26
- SIGILL: 4,
27
- SIGABRT: 6,
28
- SIGFPE: 8,
29
- SIGKILL: 9,
30
- // Darwin numbers SIGBUS 10 (Linux uses 7); this launcher refuses every
31
- // non-Darwin host before spawning a payload.
32
- SIGBUS: 10,
33
- SIGSEGV: 11,
34
- SIGPIPE: 13,
35
- SIGALRM: 14,
36
- SIGTERM: 15,
44
+ "darwin-arm64": {
45
+ SIGHUP: 1,
46
+ SIGINT: 2,
47
+ SIGQUIT: 3,
48
+ SIGILL: 4,
49
+ SIGABRT: 6,
50
+ SIGFPE: 8,
51
+ SIGKILL: 9,
52
+ SIGBUS: 10,
53
+ SIGSEGV: 11,
54
+ SIGPIPE: 13,
55
+ SIGALRM: 14,
56
+ SIGTERM: 15,
57
+ },
58
+ "linux-x64": {
59
+ SIGHUP: 1,
60
+ SIGINT: 2,
61
+ SIGQUIT: 3,
62
+ SIGILL: 4,
63
+ SIGABRT: 6,
64
+ SIGFPE: 8,
65
+ SIGKILL: 9,
66
+ SIGBUS: 7,
67
+ SIGSEGV: 11,
68
+ SIGPIPE: 13,
69
+ SIGALRM: 14,
70
+ SIGTERM: 15,
71
+ },
37
72
  };
38
73
  const QUIET_COMMANDS = new Set(["help", "--version", "--help", "-h", "-V"]);
39
74
  // The Forge skill notice belongs only to commands that use the skill or set it
@@ -142,6 +177,143 @@ function isPlainObject(value) {
142
177
  return value !== null && typeof value === "object" && !Array.isArray(value);
143
178
  }
144
179
 
180
+ // npm extracts with `mode & ~umask`, so a per-user-group umask (0002, the
181
+ // Debian/Ubuntu default) leaves the distribution tree group-writable and the
182
+ // native helper identity refuses it. Tighten only the exact platform package
183
+ // we resolved: drop group/other write bits, never add any, and leave a
184
+ // read-only install alone (its identity refusal stays honest).
185
+ // Where to address an open directory descriptor so a child lookup is resolved
186
+ // by the kernel against that descriptor, not against a path that a racing
187
+ // directory replacement could redirect.
188
+ const FD_ROOT = existsSync("/proc/self/fd") ? "/proc/self/fd" : "/dev/fd";
189
+
190
+ function descriptorPath(dirFd, name) {
191
+ return name === "" ? `${FD_ROOT}/${dirFd}` : `${FD_ROOT}/${dirFd}/${name}`;
192
+ }
193
+
194
+ // Open exactly one entry with O_NOFOLLOW and bind the descriptor to the
195
+ // metadata seen before the open. A linked entry, a metadata swap or the wrong
196
+ // file type refuses instead of being followed.
197
+ function openBoundEntry(target, expectDirectory) {
198
+ const before = lstatSync(target);
199
+ if (
200
+ before.isSymbolicLink() ||
201
+ (expectDirectory ? !before.isDirectory() : !before.isFile())
202
+ ) {
203
+ throw new Error("unexpected distribution entry");
204
+ }
205
+ const fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW);
206
+ try {
207
+ const opened = fstatSync(fd);
208
+ if (
209
+ opened.dev !== before.dev ||
210
+ opened.ino !== before.ino ||
211
+ (expectDirectory ? !opened.isDirectory() : !opened.isFile())
212
+ ) {
213
+ throw new Error("distribution entry moved between stat and open");
214
+ }
215
+ return fd;
216
+ } catch (error) {
217
+ closeSync(fd);
218
+ throw error;
219
+ }
220
+ }
221
+
222
+ function tightenDescriptor(fd) {
223
+ const metadata = fstatSync(fd);
224
+ const tightened = metadata.mode & ~0o022;
225
+ if (tightened !== metadata.mode) {
226
+ fchmodSync(fd, tightened);
227
+ }
228
+ }
229
+
230
+ function tightenEntryAt(dirFd, name, expectDirectory) {
231
+ let fd;
232
+ try {
233
+ fd = openBoundEntry(descriptorPath(dirFd, name), expectDirectory);
234
+ tightenDescriptor(fd);
235
+ } catch {
236
+ // Not ours to repair (absent, linked, swapped, or read-only). The helper
237
+ // identity still decides with the observed mode.
238
+ } finally {
239
+ if (fd !== undefined) {
240
+ try {
241
+ closeSync(fd);
242
+ } catch {
243
+ // The descriptor is already gone.
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // Walk an absolute path from `/` through descriptor-relative lookups so no
250
+ // component is resolved against a path that a racing directory replacement
251
+ // could redirect. Any linked or swapped component aborts the whole walk.
252
+ function openAbsoluteBound(path) {
253
+ if (!path.startsWith("/")) {
254
+ throw new Error("distribution root is not absolute");
255
+ }
256
+ let fd = openSync("/", constants.O_RDONLY | constants.O_NOFOLLOW);
257
+ try {
258
+ for (const component of path.split("/").filter((part) => part !== "")) {
259
+ const next = openBoundEntry(descriptorPath(fd, component), true);
260
+ closeSync(fd);
261
+ fd = next;
262
+ }
263
+ return fd;
264
+ } catch (error) {
265
+ try {
266
+ closeSync(fd);
267
+ } catch {
268
+ // The descriptor is already gone.
269
+ }
270
+ throw error;
271
+ }
272
+ }
273
+
274
+ function tightenDistributionModes(nativeDir) {
275
+ let root;
276
+ try {
277
+ root = openAbsoluteBound(nativeDir);
278
+ } catch {
279
+ // No safely bound root: touch nothing rather than risk a path that a race
280
+ // redirected. The identity refusal stays honest.
281
+ return;
282
+ }
283
+ try {
284
+ tightenDescriptor(root);
285
+ tightenEntryAt(root, MANIFEST_NAME, false);
286
+ tightenEntryAt(root, "package.json", false);
287
+ let payload;
288
+ try {
289
+ payload = openBoundEntry(descriptorPath(root, "payload"), true);
290
+ } catch {
291
+ return;
292
+ }
293
+ try {
294
+ tightenDescriptor(payload);
295
+ tightenEntryAt(payload, "aegis", false);
296
+ } finally {
297
+ closeSync(payload);
298
+ }
299
+ } finally {
300
+ closeSync(root);
301
+ }
302
+ }
303
+
304
+ function isLinuxElf64(payload) {
305
+ return (
306
+ payload.length >= 20 &&
307
+ payload[0] === 0x7f &&
308
+ payload[1] === 0x45 &&
309
+ payload[2] === 0x4c &&
310
+ payload[3] === 0x46 &&
311
+ payload[4] === ELF_CLASS_64 &&
312
+ payload[5] === ELF_DATA_LSB &&
313
+ payload.readUInt16LE(18) === EM_X86_64
314
+ );
315
+ }
316
+
145
317
  function macOSMajorVersion() {
146
318
  const system = typeof process.getSystemVersion === "function" ? process.getSystemVersion() : "";
147
319
  const fromSystem = Number.parseInt(system, 10);
@@ -155,25 +327,56 @@ function macOSMajorVersion() {
155
327
  return null;
156
328
  }
157
329
 
158
- if (process.platform !== "darwin" || process.arch !== "arm64") {
159
- refuse(
160
- `unsupported host: this launcher runs only on macOS ${MINIMUM_MACOS_MAJOR}+ arm64, ` +
161
- `found ${process.platform} ${process.arch}. No native payload was started.`,
162
- );
330
+ function hostGlibcVersion() {
331
+ const reported = process.report?.getReport?.()?.header?.glibcVersionRuntime;
332
+ return parseVersion(reported);
163
333
  }
164
- const macOSMajor = macOSMajorVersion();
165
- if (macOSMajor === null) {
166
- refuse(
167
- `unable to determine the macOS version; macOS ${MINIMUM_MACOS_MAJOR}+ arm64 is required. ` +
168
- "No native payload was started.",
169
- );
334
+
335
+ function parseVersion(value) {
336
+ if (typeof value !== "string") {
337
+ return null;
338
+ }
339
+ const match = /^(\d+)\.(\d+)/.exec(value);
340
+ if (match === null) {
341
+ return null;
342
+ }
343
+ return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10)];
344
+ }
345
+
346
+ function compareVersions(left, right) {
347
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
348
+ const leftPart = left[index] ?? 0;
349
+ const rightPart = right[index] ?? 0;
350
+ if (leftPart !== rightPart) {
351
+ return leftPart < rightPart ? -1 : 1;
352
+ }
353
+ }
354
+ return 0;
170
355
  }
171
- if (macOSMajor < MINIMUM_MACOS_MAJOR) {
356
+
357
+ const hostKey = `${process.platform}-${process.arch}`;
358
+ const supported = SUPPORTED[hostKey];
359
+ if (supported === undefined) {
172
360
  refuse(
173
- `macOS ${MINIMUM_MACOS_MAJOR} or newer is required; found macOS ${macOSMajor}. ` +
174
- "Upgrade macOS to run aegis.",
361
+ `unsupported host: this launcher runs on ${SUPPORTED_HOSTS}, ` +
362
+ `found ${process.platform} ${process.arch}. No native payload was started.`,
175
363
  );
176
364
  }
365
+ if (hostKey === "darwin-arm64") {
366
+ const macOSMajor = macOSMajorVersion();
367
+ if (macOSMajor === null) {
368
+ refuse(
369
+ `unable to determine the macOS version; macOS ${MINIMUM_MACOS_MAJOR}+ arm64 is required. ` +
370
+ "No native payload was started.",
371
+ );
372
+ }
373
+ if (macOSMajor < MINIMUM_MACOS_MAJOR) {
374
+ refuse(
375
+ `macOS ${MINIMUM_MACOS_MAJOR} or newer is required; found macOS ${macOSMajor}. ` +
376
+ "Upgrade macOS to run aegis.",
377
+ );
378
+ }
379
+ }
177
380
 
178
381
  const launcherDir = dirname(fileURLToPath(import.meta.url));
179
382
  const launcher = readJson(join(launcherDir, "package.json"), "launcher package metadata");
@@ -187,15 +390,14 @@ const scope = launcherName.endsWith(suffix) ? launcherName.slice(0, -suffix.leng
187
390
  if (!/^@[a-z0-9._-]+$/.test(scope)) {
188
391
  refuse(`launcher package name is invalid: ${JSON.stringify(launcherName)}`);
189
392
  }
190
- const nativeName = `${scope}/aegis-darwin-arm64`;
393
+ const nativeName = `${scope}/${supported.stem}`;
191
394
  if (!isPlainObject(launcher.optionalDependencies)) {
192
395
  refuse("launcher package metadata is malformed: optionalDependencies is required");
193
396
  }
194
- const dependencyKeys = Object.keys(launcher.optionalDependencies);
195
- if (dependencyKeys.length !== 1 || dependencyKeys[0] !== nativeName) {
397
+ if (!Object.prototype.hasOwnProperty.call(launcher.optionalDependencies, nativeName)) {
196
398
  refuse(
197
- `launcher declares unexpected platform dependencies: ${JSON.stringify(dependencyKeys)}; ` +
198
- `only ${nativeName} is supported`,
399
+ `launcher does not declare the host platform dependency ${nativeName}; ` +
400
+ `reinstall ${launcherName}@${launcherVersion} for ${hostKey}`,
199
401
  );
200
402
  }
201
403
  if (launcher.optionalDependencies[nativeName] !== launcherVersion) {
@@ -213,7 +415,7 @@ try {
213
415
  refuse(
214
416
  `native payload package ${nativeName}@${launcherVersion} is not installed. ` +
215
417
  `Reinstall the launcher so its optional dependency lands: ` +
216
- `npm install ${launcherName}@${launcherVersion} on a macOS ${MINIMUM_MACOS_MAJOR}+ arm64 host.`,
418
+ `npm install ${launcherName}@${launcherVersion} on ${hostKey}.`,
217
419
  );
218
420
  }
219
421
  const nativeDir = dirname(nativePackagePath);
@@ -225,6 +427,8 @@ if (native.name !== nativeName || native.version !== launcherVersion) {
225
427
  );
226
428
  }
227
429
 
430
+ tightenDistributionModes(nativeDir);
431
+
228
432
  const manifest = readJson(join(nativeDir, MANIFEST_NAME), "native manifest");
229
433
  if (manifest.schema !== MANIFEST_SCHEMA) {
230
434
  refuse(`native manifest schema is invalid: expected ${MANIFEST_SCHEMA}`);
@@ -246,14 +450,43 @@ if (
246
450
  refuse(`native manifest capability contract is invalid: expected ${CAPABILITY}`);
247
451
  }
248
452
  const compatibility = manifest.compatibility;
249
- if (
250
- !isPlainObject(compatibility) ||
251
- compatibility.os !== "darwin" ||
252
- compatibility.cpu !== "arm64" ||
253
- compatibility.libc !== "none" ||
254
- compatibility.minimum_glibc !== null
255
- ) {
256
- refuse("native manifest compatibility contract is invalid");
453
+ if (supported.kind === "mach-o-arm64") {
454
+ if (
455
+ !isPlainObject(compatibility) ||
456
+ compatibility.os !== "darwin" ||
457
+ compatibility.cpu !== "arm64" ||
458
+ compatibility.libc !== "none" ||
459
+ compatibility.minimum_glibc !== null
460
+ ) {
461
+ refuse("native manifest compatibility contract is invalid");
462
+ }
463
+ } else {
464
+ if (
465
+ !isPlainObject(compatibility) ||
466
+ compatibility.os !== "linux" ||
467
+ compatibility.cpu !== "x86_64" ||
468
+ compatibility.libc !== "glibc" ||
469
+ typeof compatibility.minimum_glibc !== "string"
470
+ ) {
471
+ refuse("native manifest compatibility contract is invalid");
472
+ }
473
+ const floor = parseVersion(compatibility.minimum_glibc);
474
+ if (floor === null) {
475
+ refuse("native manifest compatibility contract is invalid");
476
+ }
477
+ const glibc = hostGlibcVersion();
478
+ if (glibc === null) {
479
+ refuse(
480
+ `unable to determine the host glibc version; glibc ${compatibility.minimum_glibc} ` +
481
+ "or newer is required. No native payload was started.",
482
+ );
483
+ }
484
+ if (compareVersions(glibc, floor) < 0) {
485
+ refuse(
486
+ `glibc ${compatibility.minimum_glibc} or newer is required; ` +
487
+ `found ${glibc[0]}.${glibc[1]}`,
488
+ );
489
+ }
257
490
  }
258
491
 
259
492
  const payloadPath = join(nativeDir, PAYLOAD_PATH);
@@ -282,12 +515,16 @@ if (manifest.payload.byte_count !== payload.length) {
282
515
  if ((payloadStat.mode & 0o111) === 0) {
283
516
  refuse("native payload is not executable");
284
517
  }
285
- if (
286
- payload.length < 8 ||
287
- payload.readUInt32LE(0) !== MH_MAGIC_64 ||
288
- payload.readUInt32LE(4) !== CPU_TYPE_ARM64
289
- ) {
290
- refuse("native payload is not a Darwin arm64 Mach-O binary");
518
+ if (supported.kind === "mach-o-arm64") {
519
+ if (
520
+ payload.length < 8 ||
521
+ payload.readUInt32LE(0) !== MH_MAGIC_64 ||
522
+ payload.readUInt32LE(4) !== CPU_TYPE_ARM64
523
+ ) {
524
+ refuse("native payload is not a Darwin arm64 Mach-O binary");
525
+ }
526
+ } else if (!isLinuxElf64(payload)) {
527
+ refuse("native payload is not a Linux x86-64 ELF binary");
291
528
  }
292
529
 
293
530
  const argv = process.argv.slice(2);
@@ -323,7 +560,7 @@ child.on("exit", (code, signal) => {
323
560
  // Report the payload's fatal signal as a conventional exit status. Do not
324
561
  // re-raise it: the wrapper must not die by a second core-dump signal, which
325
562
  // is what a terminal sees as the foreground job (and its shell) crashing.
326
- process.exitCode = 128 + (SIGNAL_NUMBERS[signal] ?? 0);
563
+ process.exitCode = 128 + (SIGNAL_NUMBERS[hostKey]?.[signal] ?? 0);
327
564
  return;
328
565
  }
329
566
  process.exitCode = typeof code === "number" ? code : 1;
package/package.json CHANGED
@@ -5,10 +5,7 @@
5
5
  "bugs": {
6
6
  "url": "https://github.com/mithran-hq/aegis/issues"
7
7
  },
8
- "cpu": [
9
- "arm64"
10
- ],
11
- "description": "Aegis auth, gateway, and Forge CLI for macOS 15+ Apple silicon",
8
+ "description": "Aegis auth, gateway, and Forge CLI compatible with the host platform package selected at install time",
12
9
  "files": [
13
10
  "package.json",
14
11
  "aegis.mjs",
@@ -28,14 +25,12 @@
28
25
  "license": "Apache-2.0",
29
26
  "name": "@mithran-hq/aegis",
30
27
  "optionalDependencies": {
31
- "@mithran-hq/aegis-darwin-arm64": "0.9.14"
28
+ "@mithran-hq/aegis-darwin-arm64": "0.9.16",
29
+ "@mithran-hq/aegis-linux-x64": "0.9.16"
32
30
  },
33
- "os": [
34
- "darwin"
35
- ],
36
31
  "repository": {
37
32
  "type": "git",
38
33
  "url": "git+https://github.com/mithran-hq/aegis.git"
39
34
  },
40
- "version": "0.9.14"
35
+ "version": "0.9.16"
41
36
  }