@robsonai/emilyremote 0.4.0 → 0.4.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.
- package/README.md +108 -74
- package/RUNTIME_FORMAT.md +43 -6
- package/bin/emilyremote-native.cjs +0 -0
- package/lib/cli.cjs +114 -20
- package/package.json +10 -6
- package/vendor/robsonai-emilyremote-native-0.4.1-darwin-arm64.tar.gz +0 -0
- package/vendor/{robsonai-emilyremote-native-0.4.0-linux-x64.tar.gz → robsonai-emilyremote-native-0.4.1-linux-x64.tar.gz} +0 -0
package/README.md
CHANGED
|
@@ -1,91 +1,125 @@
|
|
|
1
|
-
# EmilyRemote 0.4.
|
|
1
|
+
# EmilyRemote 0.4.1
|
|
2
|
+
|
|
3
|
+
EmilyRemote lets an authorized controller assign work to a device without
|
|
4
|
+
installing a Claude or Codex agent there. This package contains the compiled
|
|
5
|
+
**device runtime** for Linux x64 and Apple Silicon Macs. It requires **Node.js 22
|
|
6
|
+
or newer and npm**; a separate Python interpreter is not required.
|
|
7
|
+
|
|
8
|
+
## Supported systems
|
|
9
|
+
|
|
10
|
+
| System | This release |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| Linux x64 servers, PCs and NUCs | Included; glibc 2.38 or newer required. |
|
|
13
|
+
| Apple Silicon Macs, including M3 | Included; macOS 26.0 or newer required by the compiled dependencies. Tested on an M3 Max running macOS 27. |
|
|
14
|
+
| Intel Macs | No qualified native build included. |
|
|
15
|
+
| Raspberry Pi / Linux ARM | No native build included. |
|
|
16
|
+
| Windows | No native build included. |
|
|
17
|
+
| Android / iPhone | This npm package is not a mobile app. |
|
|
18
|
+
| Arduino-class microcontrollers | Cannot run this host runtime; supported host-side serial/firmware workflows are a separate capability. |
|
|
19
|
+
|
|
20
|
+
Use an arm64 Node.js 22+ installation on Apple Silicon. Running x64 Node under
|
|
21
|
+
Rosetta selects the unbuilt Intel target. npm's OS/CPU filters cannot express
|
|
22
|
+
every pair in this table; `emilyremote platforms` reports the exact runtime
|
|
23
|
+
availability and setup rejects unbuilt pairs. Do not bypass those checks.
|
|
2
24
|
|
|
3
|
-
|
|
4
|
-
stable release packages the compiled **device-side** SDK for **Linux x64 with
|
|
5
|
-
glibc 2.38 or newer**. Installation and launch require **Node.js 22 or newer and
|
|
6
|
-
npm**. A separately installed Python interpreter is not needed.
|
|
25
|
+
## Install and check
|
|
7
26
|
|
|
8
|
-
|
|
9
|
-
JavaScript installer. The archive contains compiled SDK modules and required
|
|
10
|
-
runtime libraries, including CPython. It carries no SDK Python source or wheel.
|
|
11
|
-
Compiled binaries remain inspectable and can be reverse engineered.
|
|
27
|
+
After installing Node.js and npm, run:
|
|
12
28
|
|
|
13
|
-
|
|
29
|
+
```sh
|
|
30
|
+
npm install -g @robsonai/emilyremote@0.4.1
|
|
31
|
+
emilyremote setup
|
|
32
|
+
emilyremote self-check
|
|
33
|
+
emilyremote status
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run setup as your normal user. Both `emilyremote` and `emilyremote-native` invoke
|
|
37
|
+
the same launcher. Setup chooses the archive matching the host, verifies its
|
|
38
|
+
pinned SHA-256 and complete inventory, and runs its native self-check before
|
|
39
|
+
activation. It needs no network after npm installation. npm installation runs no
|
|
40
|
+
lifecycle setup or native executable.
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
|
|
42
|
+
**Setup installs the device runtime. It does not enroll a device, start a
|
|
43
|
+
service, connect to Emily, install a model or change system packages.** A
|
|
44
|
+
controller URL and appropriate device credentials/configuration are still
|
|
45
|
+
required. This package does not provide an end-user Emily login, terminal chat,
|
|
46
|
+
or automatic binding to the user's clone.
|
|
47
|
+
|
|
48
|
+
For an existing deployment, use a separate prefix to preserve its current CLI:
|
|
17
49
|
|
|
18
50
|
```sh
|
|
19
51
|
umask 077
|
|
20
|
-
install_root="$HOME/.local/share/robsonai/emilyremote-0.4"
|
|
21
|
-
npm install --prefix "$install_root/npm" --
|
|
22
|
-
|
|
52
|
+
install_root="$HOME/.local/share/robsonai/emilyremote-0.4.1"
|
|
53
|
+
npm install --prefix "$install_root/npm" --ignore-scripts --no-audit --no-fund \
|
|
54
|
+
@robsonai/emilyremote@0.4.1
|
|
23
55
|
"$install_root/npm/node_modules/.bin/emilyremote" setup --home "$install_root/state"
|
|
24
|
-
"$install_root/npm/node_modules/.bin/emilyremote" --home "$install_root/state" status
|
|
25
56
|
"$install_root/npm/node_modules/.bin/emilyremote" --home "$install_root/state" self-check
|
|
26
57
|
```
|
|
27
58
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
## Homes and upgrades
|
|
60
|
+
|
|
61
|
+
The native default home is:
|
|
62
|
+
|
|
63
|
+
- macOS: `~/Library/Application Support/RobsonAI/EmilyRemoteNative`
|
|
64
|
+
- Linux: `~/.local/share/robsonai/emilyremote-native`, or
|
|
65
|
+
`$XDG_DATA_HOME/robsonai/emilyremote-native` when configured.
|
|
66
|
+
|
|
67
|
+
`--home` or `EMILYREMOTE_NATIVE_HOME` selects another native location. Spaces in
|
|
68
|
+
paths are supported. Apple's root-owned `/var`, `/tmp` and `/etc` aliases are
|
|
69
|
+
recognized; arbitrary symlinks and unsafe ownership/permissions remain refused.
|
|
70
|
+
|
|
71
|
+
A global npm upgrade replaces the `emilyremote` command. Updating npm alone does
|
|
72
|
+
not activate a different native runtime: run setup to install the new bundled
|
|
73
|
+
runtime. Native installation state is separate from the older Python-based
|
|
74
|
+
0.3.0 home, and the earlier `EMILYREMOTE_HOME` variable is not read. No old
|
|
75
|
+
service, controller role or enrollment configuration is migrated automatically.
|
|
76
|
+
Setup refuses a home containing that older installation. Previously published
|
|
77
|
+
versions are not removed or revoked by this release.
|
|
78
|
+
|
|
79
|
+
## Device capabilities
|
|
80
|
+
|
|
81
|
+
The runtime includes shell/file tasks, PTY, tunnels, serial and firmware helpers,
|
|
82
|
+
configured local-agent activity, and signed update/supervisor machinery. Mac
|
|
83
|
+
launchd integration has been exercised on the test Mac; ordinary setup does not
|
|
84
|
+
start it. Operation needs your controller configuration and authorization. The
|
|
85
|
+
runtime uses its process owner's OS permissions; it is not a sandbox.
|
|
86
|
+
|
|
87
|
+
The package does not contain a hosted controller, Emily account credentials,
|
|
88
|
+
model weights or the optional `pymavlink` dependency. Release tests use controlled
|
|
89
|
+
fixtures and do not establish a live Emily-to-device task-and-recall loop or
|
|
90
|
+
fleet-scale qualification.
|
|
91
|
+
|
|
92
|
+
## Package and runtime integrity
|
|
93
|
+
|
|
94
|
+
Each native archive contains compiled SDK modules and the needed runtime
|
|
95
|
+
libraries, including CPython. It carries no SDK Python source or wheel. The
|
|
96
|
+
small JavaScript installer is intentionally readable. Native binaries remain
|
|
97
|
+
inspectable and can be reverse engineered.
|
|
98
|
+
|
|
99
|
+
Setup uses a private archive snapshot, complete hashed inventory, exclusive
|
|
100
|
+
lock and isolated release directory. It rejects unsafe paths, links, special
|
|
101
|
+
files, unlisted payloads and source-bearing extensions. Every launch verifies
|
|
102
|
+
the active inventory. The active pointer changes only after self-check succeeds;
|
|
103
|
+
caught activation failures restore the previous pointer. Interrupted locks and
|
|
104
|
+
failed artifacts remain for inspection.
|
|
105
|
+
|
|
106
|
+
Mac binaries and dependencies have verified ad-hoc signatures; this is not
|
|
107
|
+
Apple Developer ID signing or notarization. Mach-O architecture and deployment
|
|
108
|
+
targets are checked, and the actual host macOS version must meet the manifest.
|
|
109
|
+
The compiled Mac runtime includes a pinned public CA bundle for its default TLS
|
|
110
|
+
context unless the operator supplies an explicit trust configuration. Explicit
|
|
111
|
+
operator trust settings are preserved. Certificate and hostname verification
|
|
112
|
+
remain enabled.
|
|
113
|
+
|
|
114
|
+
An explicit local native archive requires a trusted external digest:
|
|
75
115
|
|
|
76
116
|
```sh
|
|
77
117
|
emilyremote setup --bundle /absolute/path/to/reviewed-native.tar.gz \
|
|
78
118
|
--sha256 THE_EXACT_EXTERNALLY_VERIFIED_SHA256 --home /your/private/native-home
|
|
79
119
|
```
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
variables are removed from the child environment.
|
|
87
|
-
|
|
88
|
-
The release includes the reviewed dependency inventory and upstream notices.
|
|
89
|
-
Release validation covers actual bundled installation and native functional and
|
|
90
|
-
security proofs. The stable channel is not a claim of qualification across
|
|
91
|
-
unsupported platforms or every fleet configuration.
|
|
121
|
+
A hash verifies exact bytes, not the publisher. The npm-bundled hash is pinned in
|
|
122
|
+
this installer. No separate release signature or automatic download/update
|
|
123
|
+
endpoint is configured by setup. Native command arguments are passed without a
|
|
124
|
+
shell, and Python-path/dynamic-loader injection variables are removed from child
|
|
125
|
+
environments. Dependency inventories and notices are included in each archive.
|
package/RUNTIME_FORMAT.md
CHANGED
|
@@ -14,12 +14,12 @@ runtime `version`. It must not require a separately installed Python interpreter
|
|
|
14
14
|
```json
|
|
15
15
|
{
|
|
16
16
|
"schema": "emilyremote.native-release/1",
|
|
17
|
-
"version": "0.4.
|
|
18
|
-
"platform": "
|
|
19
|
-
"arch": "
|
|
17
|
+
"version": "0.4.1",
|
|
18
|
+
"platform": "darwin",
|
|
19
|
+
"arch": "arm64",
|
|
20
20
|
"entrypoint": "emilyremote-runtime",
|
|
21
21
|
"build_manifest": "build-info.json",
|
|
22
|
-
"
|
|
22
|
+
"minimum_macos": "26.0.0",
|
|
23
23
|
"files": {
|
|
24
24
|
"emilyremote-runtime": {"sha256": "ACTUAL_EXECUTABLE_SHA256", "mode": 493},
|
|
25
25
|
"build-info.json": {"sha256": "ACTUAL_BUILD_MANIFEST_SHA256", "mode": 420}
|
|
@@ -28,8 +28,9 @@ runtime `version`. It must not require a separately installed Python interpreter
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
This example is illustrative, not a valid signed or hashed release. Mode values
|
|
31
|
-
are JSON integers (493 = octal 0755, 420 = octal 0644). The
|
|
32
|
-
must come from
|
|
31
|
+
are JSON integers (493 = octal 0755, 420 = octal 0644). The example macOS minimum is illustrative, not a supported-OS promise. The
|
|
32
|
+
actual minimum must come from all native build dependencies; Linux uses the
|
|
33
|
+
optional `minimum_glibc` field instead. The file inventory must contain every
|
|
33
34
|
regular archive file except `native-release.json` itself, including all library
|
|
34
35
|
files and the build manifest. Directory entries must be parents of listed files.
|
|
35
36
|
All hashes are lowercase SHA-256. ELF class, byte order and machine type must
|
|
@@ -48,3 +49,39 @@ Receipt fields retain that archive digest, release-manifest digest and
|
|
|
48
49
|
build-manifest digest, and distinguish bundled from operator-supplied archives.
|
|
49
50
|
There is no separate signed manifest or release key verification; no independent
|
|
50
51
|
publisher-authentication claim is made.
|
|
52
|
+
|
|
53
|
+
## macOS native extension
|
|
54
|
+
|
|
55
|
+
For a Darwin bundle the manifest uses `platform: "darwin"`, `arch: "arm64"`
|
|
56
|
+
and the exact release version `0.4.1`. It requires top-level
|
|
57
|
+
`minimum_macos`, derived from the highest macOS deployment target across all
|
|
58
|
+
native files. `minimum_glibc` is forbidden for Darwin. The build manifest records
|
|
59
|
+
the same target/version, `minimum_macos`, and `signing_verified: true` only after
|
|
60
|
+
actual code-signature checks pass.
|
|
61
|
+
|
|
62
|
+
The entrypoint must be a thin little-endian 64-bit Mach-O executable with the
|
|
63
|
+
matching generic CPU subtype. Load command bounds and macOS deployment metadata
|
|
64
|
+
are validated before execution. Each Mach-O dependency must have the same CPU
|
|
65
|
+
architecture and a minimum macOS version no higher than the manifest. Fat,
|
|
66
|
+
32-bit, arm64e-only and non-macOS Apple binaries are not qualified by this
|
|
67
|
+
release. On a real Mac the host product version is obtained using
|
|
68
|
+
`/usr/bin/sw_vers -productVersion`, rather than comparing Darwin kernel versions.
|
|
69
|
+
|
|
70
|
+
Native files may not use symbolic links inside the archive; packaging must
|
|
71
|
+
materialize or otherwise safely arrange dependencies before the final inventory
|
|
72
|
+
and signature verification. Required dependency notices remain ordinary hashed
|
|
73
|
+
payload files. The npm build availability list must remain `unbuilt` until a
|
|
74
|
+
real artifact has passed native execution and publication-content review.
|
|
75
|
+
|
|
76
|
+
The development-only `validation/check-release.cjs` gate consumes
|
|
77
|
+
`validation/native-qualification-darwin-arm64.json` with schema
|
|
78
|
+
`emilyremote.native-qualification/1`, exact `version`, `target`,
|
|
79
|
+
`bundle_sha256`, a detailed `evidence_path`, and checks marked `passed` for
|
|
80
|
+
`compiled_self_check`, `fresh_npm_install`, `both_cli_aliases`,
|
|
81
|
+
`without_host_python`, `source_and_secret_audit`, `dependency_notices`,
|
|
82
|
+
`codesign_verified`, and `native_arm64_host`. Such a receipt is an evidence index,
|
|
83
|
+
not a cryptographic attestation: a reviewer must examine the actual logs.
|
|
84
|
+
|
|
85
|
+
Mach-O constants and layout are defined in Apple's published headers:
|
|
86
|
+
- https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
|
|
87
|
+
- https://github.com/apple-oss-distributions/xnu/blob/main/osfmk/mach/machine.h
|
|
File without changes
|
package/lib/cli.cjs
CHANGED
|
@@ -9,16 +9,19 @@ const crypto = require('node:crypto');
|
|
|
9
9
|
const zlib = require('node:zlib');
|
|
10
10
|
const { spawn, spawnSync } = require('node:child_process');
|
|
11
11
|
|
|
12
|
-
const VERSION = '0.4.
|
|
12
|
+
const VERSION = '0.4.1';
|
|
13
13
|
const MAX_BUNDLE = 256 * 1024 * 1024;
|
|
14
14
|
const MAX_EXPANDED = 512 * 1024 * 1024;
|
|
15
15
|
const MANIFEST = 'native-release.json';
|
|
16
|
-
const BUILDS = Object.freeze({ 'linux-x64': 'available', 'linux-arm64': 'unbuilt', 'darwin-x64': 'unbuilt', 'darwin-arm64': '
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
const BUILDS = Object.freeze({ 'linux-x64': 'available', 'linux-arm64': 'unbuilt', 'darwin-x64': 'unbuilt', 'darwin-arm64': 'available', 'win32-x64': 'unbuilt', 'win32-arm64': 'unbuilt' });
|
|
17
|
+
// A target is enabled only after its same-version runtime is built and qualified.
|
|
18
|
+
// Never fill this map from a previous-version or different-platform archive.
|
|
19
19
|
const BUNDLED = Object.freeze({ 'linux-x64': Object.freeze({
|
|
20
|
-
file: 'vendor/robsonai-emilyremote-native-0.4.
|
|
21
|
-
sha256: '
|
|
20
|
+
file: 'vendor/robsonai-emilyremote-native-0.4.1-linux-x64.tar.gz',
|
|
21
|
+
sha256: '306da696df794b9a0a97a856bd3373fe9d060da6e7215d40b5b2000b4c0452f8',
|
|
22
|
+
}), 'darwin-arm64': Object.freeze({
|
|
23
|
+
file: 'vendor/robsonai-emilyremote-native-0.4.1-darwin-arm64.tar.gz',
|
|
24
|
+
sha256: '827043510d73113b288861838dc914df5f4442e7a8012538b096affaf3bf5e40',
|
|
22
25
|
}) });
|
|
23
26
|
const HELP = `EmilyRemote native runtime installer ${VERSION}
|
|
24
27
|
|
|
@@ -30,7 +33,8 @@ const HELP = `EmilyRemote native runtime installer ${VERSION}
|
|
|
30
33
|
|
|
31
34
|
emilyremote-native is an alias for the same command and native installation.
|
|
32
35
|
|
|
33
|
-
Setup
|
|
36
|
+
Setup chooses the matching qualified native bundle and checks its pinned SHA-256.
|
|
37
|
+
Only targets reported as available by 'emilyremote platforms' may be installed.
|
|
34
38
|
An explicit local --bundle requires an externally supplied --sha256. No host
|
|
35
39
|
Python interpreter, wheel, download host, or source fallback is used. Installing
|
|
36
40
|
this npm package runs no lifecycle setup. Other platform builds are unavailable
|
|
@@ -53,7 +57,7 @@ function platformTarget(platform = process.platform, arch = process.arch) {
|
|
|
53
57
|
return target;
|
|
54
58
|
}
|
|
55
59
|
function requireBuilt(target) {
|
|
56
|
-
if (BUILDS[target] !== 'available') throw new Error(`Native build ${target} is unavailable in this release.
|
|
60
|
+
if (BUILDS[target] !== 'available') throw new Error(`Native build ${target} is unavailable in this release. No matching qualified runtime is packaged; no source fallback is available`);
|
|
57
61
|
}
|
|
58
62
|
function resolveBundle(options, target = platformTarget(), packageRoot = path.resolve(__dirname, '..')) {
|
|
59
63
|
if (options.bundle !== undefined || options.sha256 !== undefined) {
|
|
@@ -64,14 +68,27 @@ function resolveBundle(options, target = platformTarget(), packageRoot = path.re
|
|
|
64
68
|
if (!bundled) throw new Error(`No bundled native runtime is available for ${target}; no source fallback is available`);
|
|
65
69
|
return { bundle: path.join(packageRoot, bundled.file), sha256: bundled.sha256 };
|
|
66
70
|
}
|
|
67
|
-
function defaultHome(env = process.env) {
|
|
71
|
+
function defaultHome(env = process.env, platform = process.platform, userHome = os.homedir()) {
|
|
68
72
|
if (env.EMILYREMOTE_NATIVE_HOME) return path.resolve(env.EMILYREMOTE_NATIVE_HOME);
|
|
69
|
-
if (
|
|
70
|
-
if (
|
|
71
|
-
return path.join(env.XDG_DATA_HOME || path.join(
|
|
73
|
+
if (platform === 'win32') return path.join(env.LOCALAPPDATA || path.join(userHome, 'AppData', 'Local'), 'RobsonAI', 'EmilyRemoteNative');
|
|
74
|
+
if (platform === 'darwin') return path.join(userHome, 'Library', 'Application Support', 'RobsonAI', 'EmilyRemoteNative');
|
|
75
|
+
return path.join(env.XDG_DATA_HOME || path.join(userHome, '.local', 'share'), 'robsonai', 'emilyremote-native');
|
|
72
76
|
}
|
|
73
|
-
function
|
|
77
|
+
function canonicalPath(target, platform = process.platform, filesystem = fs) {
|
|
74
78
|
target = path.resolve(target);
|
|
79
|
+
// macOS ships these root-owned links. Resolve only these exact aliases;
|
|
80
|
+
// calling realpath on an arbitrary user path would conceal unsafe symlinks.
|
|
81
|
+
const match = platform === 'darwin' && target.match(/^\/(var|tmp|etc)(?=\/|$)/);
|
|
82
|
+
if (!match) return target;
|
|
83
|
+
const alias = `/${match[1]}`, parent = filesystem.lstatSync('/');
|
|
84
|
+
const link = filesystem.lstatSync(alias);
|
|
85
|
+
if (!link.isSymbolicLink()) return target;
|
|
86
|
+
const resolved = path.resolve('/', filesystem.readlinkSync(alias));
|
|
87
|
+
if (!parent.isDirectory() || parent.uid !== 0 || (parent.mode & 0o022) || link.uid !== 0 || resolved !== `/private/${match[1]}`) throw new Error(`Unsafe macOS system alias: ${alias}`);
|
|
88
|
+
return resolved + target.slice(alias.length);
|
|
89
|
+
}
|
|
90
|
+
function checkPath(target, { ownerLeaf = true, missing = true } = {}) {
|
|
91
|
+
target = canonicalPath(target);
|
|
75
92
|
const chain = [];
|
|
76
93
|
for (let current = target;; current = path.dirname(current)) { chain.unshift(current); if (path.dirname(current) === current) break; }
|
|
77
94
|
for (const current of chain) {
|
|
@@ -103,7 +120,7 @@ function syncDir(directory) {
|
|
|
103
120
|
if (process.platform !== 'win32') { const fd = fs.openSync(directory, 'r'); try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } }
|
|
104
121
|
}
|
|
105
122
|
function atomicWrite(file, bytes) {
|
|
106
|
-
checkPath(file); ensureDir(path.dirname(file));
|
|
123
|
+
file = checkPath(file); ensureDir(path.dirname(file));
|
|
107
124
|
const temporary = `${file}.${crypto.randomBytes(10).toString('hex')}.tmp`;
|
|
108
125
|
let fd;
|
|
109
126
|
try {
|
|
@@ -186,7 +203,61 @@ function parseTar(gzip) {
|
|
|
186
203
|
if (!ended || data.subarray(offset).some((byte) => byte !== 0)) throw new Error('Native tar archive has missing end markers or trailing content');
|
|
187
204
|
return entries;
|
|
188
205
|
}
|
|
189
|
-
function
|
|
206
|
+
function versionParts(value) {
|
|
207
|
+
if (typeof value !== 'string' || !/^\d+\.\d+(?:\.\d+)?$/.test(value)) throw new Error(`Invalid macOS version: ${JSON.stringify(value)}`);
|
|
208
|
+
const result = value.split('.').map(Number);
|
|
209
|
+
if (result.some((part) => !Number.isSafeInteger(part) || part > 65535) || result[0] < 10) throw new Error('Invalid macOS version components');
|
|
210
|
+
while (result.length < 3) result.push(0);
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
function compareVersions(left, right) {
|
|
214
|
+
const a = versionParts(left), b = versionParts(right);
|
|
215
|
+
for (let index = 0; index < 3; index++) if (a[index] !== b[index]) return Math.sign(a[index] - b[index]);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
function hostMacosVersion(runner = spawnSync) {
|
|
219
|
+
const result = runner('/usr/bin/sw_vers', ['-productVersion'], { shell: false, encoding: 'utf8', timeout: 5000, maxBuffer: 16384, env: { ...cleanEnv(), SYSTEM_VERSION_COMPAT: '0' } });
|
|
220
|
+
if (result.error || result.status !== 0) throw new Error('Cannot determine macOS product version; native runtime was not executed');
|
|
221
|
+
const version = String(result.stdout).trim();
|
|
222
|
+
versionParts(version);
|
|
223
|
+
return version;
|
|
224
|
+
}
|
|
225
|
+
function verifyMachO(data, arch, executable = true) {
|
|
226
|
+
const cpu = { arm64: 0x0100000c, x64: 0x01000007 }[arch];
|
|
227
|
+
if (!cpu || data.length < 32 || data.readUInt32LE(0) !== 0xfeedfacf || data.readUInt32LE(4) !== cpu) throw new Error('Native entrypoint is not the expected thin 64-bit Mach-O architecture');
|
|
228
|
+
const subtype = data.readUInt32LE(8) & 0x00ffffff;
|
|
229
|
+
if (subtype !== (arch === 'arm64' ? 0 : 3)) throw new Error('Specialized Mach-O CPU subtypes are not qualified');
|
|
230
|
+
const type = data.readUInt32LE(12);
|
|
231
|
+
if (executable ? type !== 2 : ![2, 6, 8].includes(type)) throw new Error('Unexpected Mach-O file type');
|
|
232
|
+
const count = data.readUInt32LE(16), size = data.readUInt32LE(20), end = 32 + size;
|
|
233
|
+
if (!count || count > 65536 || size < count * 8 || end > data.length) throw new Error('Invalid Mach-O load command table');
|
|
234
|
+
let offset = 32, minimum;
|
|
235
|
+
const unpack = (packed) => `${packed >>> 16}.${(packed >>> 8) & 0xff}.${packed & 0xff}`;
|
|
236
|
+
for (let index = 0; index < count; index++) {
|
|
237
|
+
if (offset + 8 > end) throw new Error('Truncated Mach-O load command');
|
|
238
|
+
const command = data.readUInt32LE(offset), length = data.readUInt32LE(offset + 4);
|
|
239
|
+
if (length < 8 || length % 8 || offset + length > end) throw new Error('Invalid Mach-O load command size');
|
|
240
|
+
let required;
|
|
241
|
+
if (command === 0x32) {
|
|
242
|
+
if (length < 24 || data.readUInt32LE(offset + 8) !== 1 || length !== 24 + 8 * data.readUInt32LE(offset + 20)) throw new Error('Mach-O LC_BUILD_VERSION must describe macOS');
|
|
243
|
+
required = unpack(data.readUInt32LE(offset + 12));
|
|
244
|
+
} else if (command === 0x24) {
|
|
245
|
+
if (length !== 16) throw new Error('Invalid Mach-O macOS version command');
|
|
246
|
+
required = unpack(data.readUInt32LE(offset + 8));
|
|
247
|
+
} else if ([0x25, 0x2f, 0x30].includes(command)) {
|
|
248
|
+
throw new Error('Mach-O runtime targets a non-macOS Apple platform');
|
|
249
|
+
}
|
|
250
|
+
if (required) {
|
|
251
|
+
versionParts(required);
|
|
252
|
+
if (minimum !== undefined) throw new Error('Duplicate Mach-O deployment target commands');
|
|
253
|
+
minimum = required;
|
|
254
|
+
}
|
|
255
|
+
offset += length;
|
|
256
|
+
}
|
|
257
|
+
if (offset !== end || minimum === undefined) throw new Error('Mach-O load commands do not identify a macOS deployment target');
|
|
258
|
+
return { minimum_macos: minimum };
|
|
259
|
+
}
|
|
260
|
+
function verifyManifest(entries, target = platformTarget(), expectedVersion = VERSION, host = {}) {
|
|
190
261
|
const entry = entries.get(MANIFEST);
|
|
191
262
|
if (!entry || entry.type !== '0' || entry.data.length > 2 * 1024 * 1024) throw new Error('Native release manifest is missing or too large');
|
|
192
263
|
let manifest; try { manifest = JSON.parse(entry.data.toString('utf8')); } catch { throw new Error('Native release manifest is not valid JSON'); }
|
|
@@ -211,11 +282,33 @@ function verifyManifest(entries, target = platformTarget(), expectedVersion = VE
|
|
|
211
282
|
const executable = entries.get(manifest.entrypoint);
|
|
212
283
|
if (!executable || !(executable.mode & 0o100)) throw new Error('Native entrypoint is missing or not executable');
|
|
213
284
|
if (manifest.platform === 'linux' && (executable.data.length < 20 || !executable.data.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) || executable.data[4] !== 2 || executable.data[5] !== 1 || executable.data.readUInt16LE(18) !== (manifest.arch === 'x64' ? 62 : 183))) throw new Error('Native entrypoint is not the expected ELF architecture');
|
|
285
|
+
if (manifest.platform === 'darwin') {
|
|
286
|
+
const native = verifyMachO(executable.data, manifest.arch);
|
|
287
|
+
versionParts(manifest.minimum_macos);
|
|
288
|
+
if (manifest.minimum_glibc !== undefined) throw new Error('A macOS runtime cannot require glibc');
|
|
289
|
+
if (compareVersions(manifest.minimum_macos, native.minimum_macos) < 0) throw new Error('Manifest understates the Mach-O minimum macOS version');
|
|
290
|
+
// Validate every Mach-O dependency, including a higher deployment target.
|
|
291
|
+
for (const file of entries.values()) {
|
|
292
|
+
if (file.type !== '0') continue;
|
|
293
|
+
if (file.data.length < 4) {
|
|
294
|
+
if (/\.(?:dylib|so)$/.test(file.name)) throw new Error(`Unsupported Mach-O dependency format: ${file.name}`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const magic = file.data.readUInt32LE(0);
|
|
298
|
+
if (magic === 0xfeedfacf) {
|
|
299
|
+
const dependency = verifyMachO(file.data, manifest.arch, file.name === manifest.entrypoint);
|
|
300
|
+
if (compareVersions(manifest.minimum_macos, dependency.minimum_macos) < 0) throw new Error(`Manifest understates dependency minimum macOS version: ${file.name}`);
|
|
301
|
+
} else if (/\.(?:dylib|so)$/.test(file.name) || [0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca, 0xfeedface, 0xcefaedfe, 0xcffaedfe].includes(magic)) {
|
|
302
|
+
throw new Error(`Unsupported Mach-O dependency format: ${file.name}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (compareVersions(host.macosVersion ?? hostMacosVersion(), manifest.minimum_macos) < 0) throw new Error(`Native bundle requires macOS >=${manifest.minimum_macos}`);
|
|
306
|
+
} else if (manifest.minimum_macos !== undefined) throw new Error('Only a macOS runtime may declare minimum_macos');
|
|
214
307
|
if (manifest.minimum_glibc) {
|
|
215
|
-
const
|
|
308
|
+
const glibc = host.glibcVersion ?? process.report?.getReport().header.glibcVersionRuntime;
|
|
216
309
|
const parts = (text) => String(text).split('.').map(Number);
|
|
217
|
-
if (!/^\d+\.\d+$/.test(manifest.minimum_glibc) || !
|
|
218
|
-
const required = parts(manifest.minimum_glibc), actual = parts(
|
|
310
|
+
if (!/^\d+\.\d+$/.test(manifest.minimum_glibc) || !glibc || !/^\d+\.\d+$/.test(glibc)) throw new Error('This native bundle requires a compatible glibc host');
|
|
311
|
+
const required = parts(manifest.minimum_glibc), actual = parts(glibc);
|
|
219
312
|
if (actual[0] < required[0] || (actual[0] === required[0] && actual[1] < required[1])) throw new Error(`Native bundle requires glibc >=${manifest.minimum_glibc}`);
|
|
220
313
|
}
|
|
221
314
|
return { manifest, manifest_sha256: hash(entry.data), build_manifest_sha256: manifest.files[manifest.build_manifest].sha256 };
|
|
@@ -245,6 +338,7 @@ function installedEntries(root) {
|
|
|
245
338
|
visit(root); return entries;
|
|
246
339
|
}
|
|
247
340
|
function verifyInstalled(home) {
|
|
341
|
+
home = checkPath(home);
|
|
248
342
|
const activePath = path.join(home, 'native', 'active.json'); checkPath(activePath, { missing: false });
|
|
249
343
|
const active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
250
344
|
if (active.schema !== 'emilyremote.native-installation/1' || active.state !== 'READY' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(active.version || '')) throw new Error('Native installation is not READY with an exact version');
|
|
@@ -265,7 +359,7 @@ function selfCheck(runtime, runner) {
|
|
|
265
359
|
return info;
|
|
266
360
|
}
|
|
267
361
|
async function setup(home, options, { runner = spawnSync, log = console.log } = {}) {
|
|
268
|
-
const target = platformTarget(); requireBuilt(target); home =
|
|
362
|
+
const target = platformTarget(); requireBuilt(target); home = checkPath(home);
|
|
269
363
|
const selectedBundle = resolveBundle(options, target);
|
|
270
364
|
checkPath(home);
|
|
271
365
|
for (const legacy of ['active.json', 'receipts', 'runtimes']) {
|
|
@@ -343,4 +437,4 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
343
437
|
if (parsed.command === 'status') { console.log(JSON.stringify({ ...installed.active, installer_version: VERSION, runtime: installed.root }, null, 2)); return 0; }
|
|
344
438
|
return delegate(path.join(installed.root, installed.manifest.entrypoint), [parsed.command, ...parsed.args]);
|
|
345
439
|
}
|
|
346
|
-
module.exports = { VERSION, BUILDS, BUNDLED, main, parseArgs, platformTarget, resolveBundle, cleanEnv, checkPath, atomicWrite, snapshotBundle, parseTar, verifyManifest, verifyInstalled, setup };
|
|
440
|
+
module.exports = { VERSION, BUILDS, BUNDLED, main, parseArgs, platformTarget, requireBuilt, resolveBundle, cleanEnv, defaultHome, canonicalPath, checkPath, atomicWrite, snapshotBundle, parseTar, verifyMachO, compareVersions, hostMacosVersion, verifyManifest, verifyInstalled, setup };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robsonai/emilyremote",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "RobsonAI EmilyRemote compiled device runtime for Linux x64",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "RobsonAI EmilyRemote compiled device runtime for Linux x64 and Apple Silicon macOS",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22"
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
"lib/cli.cjs",
|
|
16
16
|
"README.md",
|
|
17
17
|
"RUNTIME_FORMAT.md",
|
|
18
|
-
"vendor/robsonai-emilyremote-native-0.4.
|
|
18
|
+
"vendor/robsonai-emilyremote-native-0.4.1-linux-x64.tar.gz",
|
|
19
|
+
"vendor/robsonai-emilyremote-native-0.4.1-darwin-arm64.tar.gz"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
|
-
"test": "node --test test/*.test.cjs"
|
|
22
|
+
"test": "node --test test/*.test.cjs",
|
|
23
|
+
"check-release": "node validation/check-release.cjs"
|
|
22
24
|
},
|
|
23
25
|
"publishConfig": {
|
|
24
26
|
"access": "public",
|
|
@@ -26,9 +28,11 @@
|
|
|
26
28
|
},
|
|
27
29
|
"author": "RobsonAI",
|
|
28
30
|
"os": [
|
|
29
|
-
"linux"
|
|
31
|
+
"linux",
|
|
32
|
+
"darwin"
|
|
30
33
|
],
|
|
31
34
|
"cpu": [
|
|
32
|
-
"x64"
|
|
35
|
+
"x64",
|
|
36
|
+
"arm64"
|
|
33
37
|
]
|
|
34
38
|
}
|
|
Binary file
|
|
index 09c1f3c..1fe5b6d 100644
|
|
|
Binary file
|