@robsonai/emilyremote 0.3.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 +115 -115
- package/RUNTIME_FORMAT.md +87 -0
- package/bin/{emilyremote.cjs → emilyremote-native.cjs} +0 -0
- package/lib/cli.cjs +389 -231
- package/package.json +34 -9
- package/vendor/robsonai-emilyremote-native-0.4.1-darwin-arm64.tar.gz +0 -0
- package/vendor/robsonai-emilyremote-native-0.4.1-linux-x64.tar.gz +0 -0
- package/PROVENANCE.json +0 -18
- package/scripts/verify_install.py +0 -48
- package/vendor/robsonai_emilyremote-0.3.0-py3-none-any.whl +0 -0
package/README.md
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
# EmilyRemote
|
|
1
|
+
# EmilyRemote 0.4.1
|
|
2
2
|
|
|
3
|
-
EmilyRemote
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
name is **EmilyRemote**; npm package names use lowercase. Organization ownership
|
|
8
|
-
and publication are pending verification. The examples below become public
|
|
9
|
-
installation commands after publication.
|
|
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.
|
|
10
7
|
|
|
11
|
-
##
|
|
8
|
+
## Supported systems
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Install and check
|
|
26
|
+
|
|
27
|
+
After installing Node.js and npm, run:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npm install -g @robsonai/emilyremote@0.4.1
|
|
31
|
+
emilyremote setup
|
|
20
32
|
emilyremote self-check
|
|
33
|
+
emilyremote status
|
|
21
34
|
```
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
41
|
+
|
|
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:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
umask 077
|
|
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
|
|
55
|
+
"$install_root/npm/node_modules/.bin/emilyremote" setup --home "$install_root/state"
|
|
56
|
+
"$install_root/npm/node_modules/.bin/emilyremote" --home "$install_root/state" self-check
|
|
28
57
|
```
|
|
29
58
|
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
emilyremote setup --bundle /absolute/path/to/reviewed-native.tar.gz \
|
|
118
|
+
--sha256 THE_EXACT_EXTERNALLY_VERIFIED_SHA256 --home /your/private/native-home
|
|
90
119
|
```
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Only commands explicitly requested by the caller run. The SDK's shell capability
|
|
98
|
-
uses that process owner's OS permissions; this wrapper is not a sandbox. The
|
|
99
|
-
wrapper's `status` reports package readiness, not a running service or enrollment.
|
|
100
|
-
|
|
101
|
-
## Supported scope and validation
|
|
102
|
-
|
|
103
|
-
This package installs the Python host runtime on systems that can provide the
|
|
104
|
-
prerequisites. Linux package installation and self-check are verified for this
|
|
105
|
-
release. macOS and Windows paths and launcher handling are implemented but still
|
|
106
|
-
require native qualification. Core installation
|
|
107
|
-
does not include the optional `cryptography`, `pyserial`, or `pymavlink` packages,
|
|
108
|
-
local model weights, Android APK, Arduino/ESP32 firmware, or a controller service.
|
|
109
|
-
It makes no claim that Emily can dispatch a task until that integration is tested.
|
|
110
|
-
|
|
111
|
-
## Draft publication procedure
|
|
112
|
-
|
|
113
|
-
1. Verify the authenticated account has publish rights for the `robsonai`
|
|
114
|
-
organization and target `@robsonai/emilyremote@0.3.0`. Review licensing before
|
|
115
|
-
publishing; the package currently declares `UNLICENSED`.
|
|
116
|
-
2. Run `npm test`, `npm pack --json`, and test installation of the resulting
|
|
117
|
-
tarball into a fresh prefix and fresh EmilyRemote home.
|
|
118
|
-
3. Review the packed file list and provenance. It contains the SDK wheel and
|
|
119
|
-
wrapper, with no node credentials or private keys.
|
|
120
|
-
4. From the verified package directory, use `npm publish --access public` only
|
|
121
|
-
after publication is authorized and the registry authentication is ready.
|
|
122
|
-
5. Verify the registry tarball integrity and repeat installation using the public
|
|
123
|
-
package name. Do not describe the npm URL as live before that check succeeds.
|
|
124
|
-
|
|
125
|
-
`PROVENANCE.json` records the release wheel identity and historical distribution.
|
|
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.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Native bundle contract — schema 1
|
|
2
|
+
|
|
3
|
+
A `.tar.gz` archive in standard USTAR format contains regular files/directories,
|
|
4
|
+
with no leading `./` root. Only relative paths are accepted. Links, PAX/GNU
|
|
5
|
+
extensions, special files, duplicate names, unsafe modes and trailing nonzero
|
|
6
|
+
content are rejected. Current limits: 256 MiB compressed, 512 MiB expanded,
|
|
7
|
+
20,000 entries, 2 MiB release manifest.
|
|
8
|
+
|
|
9
|
+
The root contains `emilyremote-runtime`, its dependency libraries and
|
|
10
|
+
`native-release.json`. The executable receives normal SDK arguments directly;
|
|
11
|
+
`emilyremote-runtime self-check` must emit JSON with `ok: true` and the exact
|
|
12
|
+
runtime `version`. It must not require a separately installed Python interpreter.
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"schema": "emilyremote.native-release/1",
|
|
17
|
+
"version": "0.4.1",
|
|
18
|
+
"platform": "darwin",
|
|
19
|
+
"arch": "arm64",
|
|
20
|
+
"entrypoint": "emilyremote-runtime",
|
|
21
|
+
"build_manifest": "build-info.json",
|
|
22
|
+
"minimum_macos": "26.0.0",
|
|
23
|
+
"files": {
|
|
24
|
+
"emilyremote-runtime": {"sha256": "ACTUAL_EXECUTABLE_SHA256", "mode": 493},
|
|
25
|
+
"build-info.json": {"sha256": "ACTUAL_BUILD_MANIFEST_SHA256", "mode": 420}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
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 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
|
|
34
|
+
regular archive file except `native-release.json` itself, including all library
|
|
35
|
+
files and the build manifest. Directory entries must be parents of listed files.
|
|
36
|
+
All hashes are lowercase SHA-256. ELF class, byte order and machine type must
|
|
37
|
+
match the declared Linux target.
|
|
38
|
+
|
|
39
|
+
The build manifest is hashed as an ordinary payload file. It should record
|
|
40
|
+
version, target, compiler/toolchain identity and relevant reproducible build
|
|
41
|
+
inputs without workstation paths, credentials, proprietary source, caches or
|
|
42
|
+
unnecessary user information. Its public contents need a separate review; a
|
|
43
|
+
correct digest does not make private information safe to publish.
|
|
44
|
+
|
|
45
|
+
The default archive SHA-256 is pinned in the npm installer alongside its exact
|
|
46
|
+
bundled file path. For an explicitly supplied local archive, its SHA-256 must be
|
|
47
|
+
supplied through `setup --sha256`, with the file supplied through `--bundle`.
|
|
48
|
+
Receipt fields retain that archive digest, release-manifest digest and
|
|
49
|
+
build-manifest digest, and distinguish bundled from operator-supplied archives.
|
|
50
|
+
There is no separate signed manifest or release key verification; no independent
|
|
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
|
@@ -1,282 +1,440 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Public JavaScript installation/launch plumbing. SDK implementation is compiled
|
|
4
|
+
// in the separately inventoried vendor runtime, never shipped here as source.
|
|
3
5
|
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
6
|
const path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
6
8
|
const crypto = require('node:crypto');
|
|
9
|
+
const zlib = require('node:zlib');
|
|
7
10
|
const { spawn, spawnSync } = require('node:child_process');
|
|
8
11
|
|
|
9
|
-
const VERSION = '0.
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const VERSION = '0.4.1';
|
|
13
|
+
const MAX_BUNDLE = 256 * 1024 * 1024;
|
|
14
|
+
const MAX_EXPANDED = 512 * 1024 * 1024;
|
|
15
|
+
const MANIFEST = 'native-release.json';
|
|
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
|
+
const BUNDLED = Object.freeze({ 'linux-x64': Object.freeze({
|
|
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',
|
|
25
|
+
}) });
|
|
26
|
+
const HELP = `EmilyRemote native runtime installer ${VERSION}
|
|
14
27
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
emilyremote setup [--python PATH] [--home DIRECTORY]
|
|
18
|
-
emilyremote [--home DIRECTORY] <SDK command> [arguments...]
|
|
28
|
+
emilyremote setup [--home DIRECTORY]
|
|
29
|
+
emilyremote setup --bundle FILE.tar.gz --sha256 HEX [--home DIRECTORY]
|
|
19
30
|
emilyremote status [--home DIRECTORY]
|
|
31
|
+
emilyremote platforms
|
|
32
|
+
emilyremote [--home DIRECTORY] <runtime command> [arguments...]
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
virtualenv. Python 3.11+ with venv/ensurepip is required. Installation is offline.
|
|
23
|
-
Installing this npm package does not start setup, services, or enrollment.
|
|
24
|
-
|
|
25
|
-
Examples:
|
|
26
|
-
emilyremote setup --python python3
|
|
27
|
-
emilyremote self-check
|
|
28
|
-
emilyremote node --config /path/to/node.json
|
|
34
|
+
emilyremote-native is an alias for the same command and native installation.
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
38
|
+
An explicit local --bundle requires an externally supplied --sha256. No host
|
|
39
|
+
Python interpreter, wheel, download host, or source fallback is used. Installing
|
|
40
|
+
this npm package runs no lifecycle setup. Other platform builds are unavailable
|
|
41
|
+
until built and qualified. The small public installer is readable JavaScript;
|
|
42
|
+
compiled binaries can still be inspected and reverse engineered.
|
|
34
43
|
`;
|
|
35
44
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
function hash(data) { return crypto.createHash('sha256').update(data).digest('hex'); }
|
|
46
|
+
function sha(file) { return hash(fs.readFileSync(file)); }
|
|
47
|
+
function exists(file) { try { fs.lstatSync(file); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } }
|
|
48
|
+
function cleanEnv(source = process.env) {
|
|
49
|
+
const env = { ...source };
|
|
50
|
+
for (const key of Object.keys(env)) if (/^(PYTHON|PIP_|NODE_OPTIONS$|NODE_PATH$|LD_PRELOAD$|LD_LIBRARY_PATH$|DYLD_)/i.test(key)) delete env[key];
|
|
51
|
+
env.PYTHONDONTWRITEBYTECODE = '1';
|
|
52
|
+
return env;
|
|
53
|
+
}
|
|
54
|
+
function platformTarget(platform = process.platform, arch = process.arch) {
|
|
55
|
+
const target = `${platform}-${arch}`;
|
|
56
|
+
if (!Object.hasOwn(BUILDS, target)) throw new Error(`Unsupported native platform ${target}; no source fallback is available`);
|
|
57
|
+
return target;
|
|
58
|
+
}
|
|
59
|
+
function requireBuilt(target) {
|
|
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`);
|
|
61
|
+
}
|
|
62
|
+
function resolveBundle(options, target = platformTarget(), packageRoot = path.resolve(__dirname, '..')) {
|
|
63
|
+
if (options.bundle !== undefined || options.sha256 !== undefined) {
|
|
64
|
+
if (!options.bundle || !/^[a-fA-F0-9]{64}$/.test(options.sha256 || '')) throw new Error('An explicit --bundle requires an explicit 64-character --sha256; both options must be provided together');
|
|
65
|
+
return { bundle: path.resolve(options.bundle), sha256: options.sha256.toLowerCase() };
|
|
47
66
|
}
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
const bundled = BUNDLED[target];
|
|
68
|
+
if (!bundled) throw new Error(`No bundled native runtime is available for ${target}; no source fallback is available`);
|
|
69
|
+
return { bundle: path.join(packageRoot, bundled.file), sha256: bundled.sha256 };
|
|
50
70
|
}
|
|
51
|
-
|
|
52
71
|
function defaultHome(env = process.env, platform = process.platform, userHome = os.homedir()) {
|
|
53
|
-
if (env.
|
|
54
|
-
if (platform === 'win32') return path.join(env.LOCALAPPDATA || path.join(userHome, 'AppData', 'Local'), 'RobsonAI', '
|
|
55
|
-
if (platform === 'darwin') return path.join(userHome, 'Library', 'Application Support', 'RobsonAI', '
|
|
56
|
-
return path.join(env.XDG_DATA_HOME || path.join(userHome, '.local', 'share'), 'robsonai', 'emilyremote');
|
|
72
|
+
if (env.EMILYREMOTE_NATIVE_HOME) return path.resolve(env.EMILYREMOTE_NATIVE_HOME);
|
|
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');
|
|
57
76
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
function canonicalPath(target, platform = process.platform, filesystem = fs) {
|
|
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);
|
|
69
89
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const parent = path.dirname(current);
|
|
87
|
-
if (parent === current) break;
|
|
88
|
-
current = parent;
|
|
90
|
+
function checkPath(target, { ownerLeaf = true, missing = true } = {}) {
|
|
91
|
+
target = canonicalPath(target);
|
|
92
|
+
const chain = [];
|
|
93
|
+
for (let current = target;; current = path.dirname(current)) { chain.unshift(current); if (path.dirname(current) === current) break; }
|
|
94
|
+
for (const current of chain) {
|
|
95
|
+
let stat;
|
|
96
|
+
try { stat = fs.lstatSync(current); } catch (error) { if (missing && error.code === 'ENOENT') continue; throw error; }
|
|
97
|
+
const leaf = current === target;
|
|
98
|
+
if (stat.isSymbolicLink()) throw new Error(`Symlink refused in native installation: ${current}`);
|
|
99
|
+
if (!leaf && !stat.isDirectory()) throw new Error(`Installation ancestor is not a directory: ${current}`);
|
|
100
|
+
if (process.platform === 'win32') throw new Error('Windows native installation is unbuilt; ACL qualification is required first');
|
|
101
|
+
const owners = leaf && ownerLeaf ? [process.getuid()] : [0, process.getuid()];
|
|
102
|
+
if (!owners.includes(stat.uid)) throw new Error(`Unsafe installation owner: ${current}`);
|
|
103
|
+
const stickyParent = !leaf && stat.isDirectory() && stat.uid === 0 && (stat.mode & 0o1000);
|
|
104
|
+
if ((stat.mode & 0o022) && !stickyParent) throw new Error(`Installation path is writable by other users: ${current}`);
|
|
105
|
+
if (leaf && !stat.isDirectory() && !stat.isFile()) throw new Error(`Unsupported installation file type: ${current}`);
|
|
89
106
|
}
|
|
107
|
+
return target;
|
|
90
108
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const stat = fs.lstatSync(current);
|
|
100
|
-
if (stat.isSymbolicLink()) throw new Error(`Symlink refused inside EmilyRemote home: ${current}`);
|
|
101
|
-
if (process.platform !== 'win32' && stat.uid !== process.getuid()) throw new Error(`EmilyRemote state must belong to the current user: ${current}`);
|
|
102
|
-
if (process.platform !== 'win32' && (stat.mode & 0o022)) throw new Error(`EmilyRemote state must not be writable by other users: ${current}`);
|
|
103
|
-
}
|
|
104
|
-
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
109
|
+
function ensureDir(directory) {
|
|
110
|
+
directory = checkPath(directory);
|
|
111
|
+
const pending = [];
|
|
112
|
+
for (let current = directory; !exists(current); current = path.dirname(current)) pending.unshift(current);
|
|
113
|
+
for (const current of pending) {
|
|
114
|
+
try { fs.mkdirSync(current, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
115
|
+
checkPath(current, { missing: false });
|
|
105
116
|
}
|
|
117
|
+
if (!fs.statSync(directory).isDirectory()) throw new Error(`Expected installation directory: ${directory}`);
|
|
118
|
+
}
|
|
119
|
+
function syncDir(directory) {
|
|
120
|
+
if (process.platform !== 'win32') { const fd = fs.openSync(directory, 'r'); try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } }
|
|
106
121
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const temp = `${destination}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
122
|
+
function atomicWrite(file, bytes) {
|
|
123
|
+
file = checkPath(file); ensureDir(path.dirname(file));
|
|
124
|
+
const temporary = `${file}.${crypto.randomBytes(10).toString('hex')}.tmp`;
|
|
111
125
|
let fd;
|
|
112
126
|
try {
|
|
113
|
-
fd = fs.openSync(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
fs.renameSync(temp, destination);
|
|
117
|
-
if (process.platform !== 'win32') { const parent = fs.openSync(path.dirname(destination), 'r'); try { fs.fsyncSync(parent); } finally { fs.closeSync(parent); } }
|
|
118
|
-
} finally {
|
|
119
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
120
|
-
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
121
|
-
}
|
|
127
|
+
fd = fs.openSync(temporary, 'wx', 0o600); fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
|
|
128
|
+
checkPath(file); fs.renameSync(temporary, file); syncDir(path.dirname(file));
|
|
129
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); if (exists(temporary)) fs.unlinkSync(temporary); }
|
|
122
130
|
}
|
|
131
|
+
function writeJson(file, value) { atomicWrite(file, Buffer.from(JSON.stringify(value, null, 2) + '\n')); }
|
|
132
|
+
function optionalBytes(file) { checkPath(file); return exists(file) ? fs.readFileSync(file) : null; }
|
|
133
|
+
function restore(file, bytes) { if (bytes === null) { checkPath(file); if (exists(file)) fs.unlinkSync(file); syncDir(path.dirname(file)); } else atomicWrite(file, bytes); }
|
|
123
134
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
function snapshotBundle(source, destination, expectedSHA) {
|
|
136
|
+
if (!/^[a-fA-F0-9]{64}$/.test(expectedSHA || '')) throw new Error('An explicit 64-character --sha256 is required for the native bundle');
|
|
137
|
+
if (fs.lstatSync(source).isSymbolicLink()) throw new Error('Native bundle source must not be a symlink');
|
|
138
|
+
checkPath(destination); ensureDir(path.dirname(destination));
|
|
139
|
+
let input, output, created = false;
|
|
140
|
+
const digest = crypto.createHash('sha256'); let size = 0;
|
|
141
|
+
try {
|
|
142
|
+
input = fs.openSync(source, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0));
|
|
143
|
+
if (!fs.fstatSync(input).isFile()) throw new Error('Native bundle source must be a regular file');
|
|
144
|
+
output = fs.openSync(destination, 'wx', 0o600); created = true;
|
|
145
|
+
const chunk = Buffer.allocUnsafe(1024 * 1024);
|
|
146
|
+
for (;;) {
|
|
147
|
+
const count = fs.readSync(input, chunk, 0, chunk.length, null); if (!count) break;
|
|
148
|
+
size += count; if (size > MAX_BUNDLE) throw new Error('Native bundle exceeds the 256 MiB compressed size limit');
|
|
149
|
+
digest.update(chunk.subarray(0, count));
|
|
150
|
+
let written = 0; while (written < count) written += fs.writeSync(output, chunk, written, count - written);
|
|
151
|
+
}
|
|
152
|
+
fs.fsyncSync(output); fs.closeSync(output); output = undefined;
|
|
153
|
+
const actual = digest.digest('hex');
|
|
154
|
+
if (actual !== expectedSHA.toLowerCase()) throw new Error(`Native bundle SHA-256 mismatch: expected ${expectedSHA}, got ${actual}`);
|
|
155
|
+
fs.chmodSync(destination, 0o400);
|
|
156
|
+
return { path: destination, sha256: actual, bytes: size };
|
|
157
|
+
} catch (error) { if (output !== undefined) { fs.closeSync(output); output = undefined; } if (created && exists(destination)) fs.unlinkSync(destination); throw error; }
|
|
158
|
+
finally { if (input !== undefined) fs.closeSync(input); if (output !== undefined) fs.closeSync(output); }
|
|
159
|
+
}
|
|
160
|
+
function tarString(block, offset, length) { return block.subarray(offset, offset + length).toString('utf8').replace(/\0.*$/s, ''); }
|
|
161
|
+
function tarOctal(block, offset, length) {
|
|
162
|
+
const value = tarString(block, offset, length).trim();
|
|
163
|
+
if (!/^[0-7]*$/.test(value)) throw new Error('Non-octal tar numeric field is unsupported');
|
|
164
|
+
const result = Number.parseInt(value || '0', 8);
|
|
165
|
+
if (!Number.isSafeInteger(result)) throw new Error('Unsafe tar numeric value');
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
function safeRelative(name) {
|
|
169
|
+
if (typeof name !== 'string' || !name || name.includes('\\') || name.includes(':') || name.startsWith('/') || /[\x00-\x1f\x7f]/.test(name)) throw new Error(`Unsafe native bundle path: ${JSON.stringify(name)}`);
|
|
170
|
+
const pieces = name.split('/');
|
|
171
|
+
if (pieces.some((part) => !part || part === '.' || part === '..')) throw new Error(`Unsafe native bundle path: ${name}`);
|
|
172
|
+
if (pieces.some((part) => part.startsWith('.'))) throw new Error(`Hidden path is not permitted in the native payload: ${name}`);
|
|
173
|
+
if (/\.(?:py|pyc|pyo|pyi|whl|map|c|cc|cpp|cxx|h|hpp|rs|go|java|js|mjs|cjs|ts|tsx)$/i.test(name)) throw new Error(`Source-bearing file is not permitted in the native payload: ${name}`);
|
|
174
|
+
return name;
|
|
130
175
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
function parseTar(gzip) {
|
|
177
|
+
if (gzip.length > MAX_BUNDLE) throw new Error('Native bundle exceeds compressed size limit');
|
|
178
|
+
const data = zlib.gunzipSync(gzip, { maxOutputLength: MAX_EXPANDED });
|
|
179
|
+
const entries = new Map(); let offset = 0, ended = false;
|
|
180
|
+
while (offset + 512 <= data.length) {
|
|
181
|
+
const header = data.subarray(offset, offset + 512); offset += 512;
|
|
182
|
+
if (header.every((byte) => byte === 0)) { ended = true; break; }
|
|
183
|
+
if (entries.size >= 20000) throw new Error('Native bundle contains too many entries');
|
|
184
|
+
const expected = tarOctal(header, 148, 8);
|
|
185
|
+
let actual = 0; for (let index = 0; index < 512; index++) actual += index >= 148 && index < 156 ? 32 : header[index];
|
|
186
|
+
if (actual !== expected) throw new Error('Native tar header checksum mismatch');
|
|
187
|
+
const magic = tarString(header, 257, 6); if (magic !== 'ustar') throw new Error('Native bundle requires standard USTAR format');
|
|
188
|
+
const prefix = tarString(header, 345, 155);
|
|
189
|
+
const raw = `${prefix ? prefix + '/' : ''}${tarString(header, 0, 100)}`;
|
|
190
|
+
const type = header[156] === 0 ? '0' : String.fromCharCode(header[156]);
|
|
191
|
+
if (!['0', '5'].includes(type)) throw new Error('Native bundle links, PAX extensions and special files are forbidden');
|
|
192
|
+
const name = safeRelative(type === '5' ? raw.replace(/\/$/, '') : raw);
|
|
193
|
+
if (entries.has(name)) throw new Error(`Duplicate native bundle path: ${name}`);
|
|
194
|
+
const mode = tarOctal(header, 100, 8);
|
|
195
|
+
if (mode & 0o7022) throw new Error(`Unsafe native file mode: ${name}`);
|
|
196
|
+
if (type === '0' && !(mode & 0o400)) throw new Error(`Native file is not readable by its owner: ${name}`);
|
|
197
|
+
const size = tarOctal(header, 124, 12);
|
|
198
|
+
if (type === '5' && size !== 0) throw new Error('Native tar directory contains data');
|
|
199
|
+
if (offset + size > data.length) throw new Error('Truncated native tar member');
|
|
200
|
+
entries.set(name, { name, type, mode, data: data.subarray(offset, offset + size) });
|
|
201
|
+
offset += Math.ceil(size / 512) * 512;
|
|
145
202
|
}
|
|
146
|
-
|
|
203
|
+
if (!ended || data.subarray(offset).some((byte) => byte !== 0)) throw new Error('Native tar archive has missing end markers or trailing content');
|
|
204
|
+
return entries;
|
|
147
205
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
155
|
-
if (receipt.state !== 'READY' || receipt.version !== VERSION || receipt.wheel_sha256 !== WHEEL_SHA) throw new Error('Installation receipt is not READY for this SDK version; run emilyremote setup');
|
|
156
|
-
if (typeof receipt.runtime_relative !== 'string' || path.isAbsolute(receipt.runtime_relative)) throw new Error('Invalid runtime path in installation receipt');
|
|
157
|
-
const root = path.resolve(home, 'runtimes', VERSION);
|
|
158
|
-
const runtime = path.resolve(home, receipt.runtime_relative);
|
|
159
|
-
if (!runtime.startsWith(root + path.sep) || runtime === root) throw new Error('Runtime path escapes the versioned installation directory');
|
|
160
|
-
checkHomePath(home, runtime);
|
|
161
|
-
if (!fs.existsSync(runtime) || !fs.realpathSync(runtime).startsWith(fs.realpathSync(root) + path.sep)) throw new Error('Runtime directory is missing or escapes its installation');
|
|
162
|
-
return { receipt, runtime };
|
|
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;
|
|
163
212
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
167
217
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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;
|
|
177
256
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 = {}) {
|
|
261
|
+
const entry = entries.get(MANIFEST);
|
|
262
|
+
if (!entry || entry.type !== '0' || entry.data.length > 2 * 1024 * 1024) throw new Error('Native release manifest is missing or too large');
|
|
263
|
+
let manifest; try { manifest = JSON.parse(entry.data.toString('utf8')); } catch { throw new Error('Native release manifest is not valid JSON'); }
|
|
264
|
+
if (manifest.schema !== 'emilyremote.native-release/1' || manifest.version !== expectedVersion) throw new Error('Native manifest schema/version does not match the expected release');
|
|
265
|
+
if (`${manifest.platform}-${manifest.arch}` !== target) throw new Error(`Native bundle platform does not match host ${target}`);
|
|
266
|
+
if (manifest.entrypoint !== 'emilyremote-runtime') throw new Error('Unexpected native runtime entrypoint');
|
|
267
|
+
if (!manifest.files || Array.isArray(manifest.files) || typeof manifest.files !== 'object') throw new Error('Native manifest requires a file inventory');
|
|
268
|
+
if (Object.hasOwn(manifest.files, MANIFEST)) throw new Error('Manifest must not recursively inventory itself');
|
|
269
|
+
if (typeof manifest.build_manifest !== 'string' || !Object.hasOwn(manifest.files, manifest.build_manifest)) throw new Error('Native build manifest must be present in the hashed inventory');
|
|
270
|
+
for (const [name, expected] of Object.entries(manifest.files)) {
|
|
271
|
+
safeRelative(name);
|
|
272
|
+
const file = entries.get(name);
|
|
273
|
+
if (!file || file.type !== '0') throw new Error(`Native manifest file missing: ${name}`);
|
|
274
|
+
if (!expected || !/^[a-f0-9]{64}$/.test(expected.sha256 || '') || !Number.isInteger(expected.mode)) throw new Error(`Invalid native inventory entry: ${name}`);
|
|
275
|
+
if (expected.mode !== file.mode || (expected.mode & 0o7022)) throw new Error(`Native inventory mode mismatch: ${name}`);
|
|
276
|
+
if (hash(file.data) !== expected.sha256) throw new Error(`Native inventory SHA-256 mismatch: ${name}`);
|
|
277
|
+
}
|
|
278
|
+
for (const [name, file] of entries) {
|
|
279
|
+
if (file.type === '0' && name !== MANIFEST && !Object.hasOwn(manifest.files, name)) throw new Error(`Unlisted native payload file: ${name}`);
|
|
280
|
+
if (file.type === '5' && !Object.keys(manifest.files).some((item) => item.startsWith(name + '/'))) throw new Error(`Unlisted native payload directory: ${name}`);
|
|
281
|
+
}
|
|
282
|
+
const executable = entries.get(manifest.entrypoint);
|
|
283
|
+
if (!executable || !(executable.mode & 0o100)) throw new Error('Native entrypoint is missing or not executable');
|
|
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}`);
|
|
192
303
|
}
|
|
193
304
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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');
|
|
307
|
+
if (manifest.minimum_glibc) {
|
|
308
|
+
const glibc = host.glibcVersion ?? process.report?.getReport().header.glibcVersionRuntime;
|
|
309
|
+
const parts = (text) => String(text).split('.').map(Number);
|
|
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);
|
|
312
|
+
if (actual[0] < required[0] || (actual[0] === required[0] && actual[1] < required[1])) throw new Error(`Native bundle requires glibc >=${manifest.minimum_glibc}`);
|
|
313
|
+
}
|
|
314
|
+
return { manifest, manifest_sha256: hash(entry.data), build_manifest_sha256: manifest.files[manifest.build_manifest].sha256 };
|
|
315
|
+
}
|
|
316
|
+
function extract(entries, root) {
|
|
317
|
+
ensureDir(root);
|
|
318
|
+
for (const [name, item] of entries) {
|
|
319
|
+
const destination = path.join(root, ...name.split('/'));
|
|
320
|
+
checkPath(destination);
|
|
321
|
+
if (item.type === '5') ensureDir(destination);
|
|
322
|
+
else { ensureDir(path.dirname(destination)); const fd = fs.openSync(destination, 'wx', item.mode); try { fs.writeFileSync(fd, item.data); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } fs.chmodSync(destination, item.mode); }
|
|
323
|
+
}
|
|
324
|
+
syncDir(root);
|
|
325
|
+
}
|
|
326
|
+
function installedEntries(root) {
|
|
327
|
+
checkPath(root, { missing: false }); const entries = new Map(); let total = 0;
|
|
328
|
+
function visit(directory) {
|
|
329
|
+
for (const name of fs.readdirSync(directory)) {
|
|
330
|
+
const file = path.join(directory, name); checkPath(file, { missing: false });
|
|
331
|
+
const info = fs.lstatSync(file); const relative = path.relative(root, file).split(path.sep).join('/'); safeRelative(relative);
|
|
332
|
+
if (entries.size >= 20000) throw new Error('Installed runtime contains too many files');
|
|
333
|
+
if (info.isDirectory()) { entries.set(relative, { name: relative, type: '5', mode: info.mode & 0o777, data: Buffer.alloc(0) }); visit(file); }
|
|
334
|
+
else if (info.isFile()) { total += info.size; if (total > MAX_EXPANDED) throw new Error('Installed runtime exceeds the expanded size limit'); entries.set(relative, { name: relative, type: '0', mode: info.mode & 0o7777, data: fs.readFileSync(file) }); }
|
|
335
|
+
else throw new Error(`Unsupported installed native file: ${relative}`);
|
|
210
336
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
337
|
+
}
|
|
338
|
+
visit(root); return entries;
|
|
339
|
+
}
|
|
340
|
+
function verifyInstalled(home) {
|
|
341
|
+
home = checkPath(home);
|
|
342
|
+
const activePath = path.join(home, 'native', 'active.json'); checkPath(activePath, { missing: false });
|
|
343
|
+
const active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
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');
|
|
345
|
+
const target = platformTarget(); requireBuilt(target);
|
|
346
|
+
if (active.target !== target || !/^[a-f0-9]{64}$/.test(active.bundle_sha256 || '')) throw new Error('Native installation identity is invalid');
|
|
347
|
+
if (typeof active.runtime_relative !== 'string' || path.isAbsolute(active.runtime_relative)) throw new Error('Invalid native runtime path');
|
|
348
|
+
const root = path.resolve(home, active.runtime_relative); const base = path.join(path.resolve(home), 'native', 'releases') + path.sep;
|
|
349
|
+
if (!root.startsWith(base) || !root.endsWith(path.sep + 'runtime')) throw new Error('Native runtime path escapes its installation');
|
|
350
|
+
const verified = verifyManifest(installedEntries(root), target, active.version);
|
|
351
|
+
if (verified.manifest_sha256 !== active.manifest_sha256 || verified.build_manifest_sha256 !== active.build_manifest_sha256) throw new Error('Installed native manifest identity changed');
|
|
352
|
+
return { active, root, ...verified };
|
|
353
|
+
}
|
|
354
|
+
function selfCheck(runtime, runner) {
|
|
355
|
+
const result = runner(path.join(runtime, 'emilyremote-runtime'), ['self-check'], { shell: false, encoding: 'utf8', timeout: 60000, maxBuffer: 2 * 1024 * 1024, env: cleanEnv(), cwd: runtime, windowsHide: true });
|
|
356
|
+
if (result.error || result.status !== 0) throw new Error(`Native runtime self-check failed: ${result.error?.message || String(result.stderr || result.stdout || result.signal || result.status).slice(-2000)}`);
|
|
357
|
+
let info; try { info = JSON.parse(result.stdout); } catch { throw new Error('Native self-check did not return JSON'); }
|
|
358
|
+
if (info.ok !== true || info.version !== VERSION) throw new Error('Native self-check version/readiness mismatch');
|
|
359
|
+
return info;
|
|
360
|
+
}
|
|
361
|
+
async function setup(home, options, { runner = spawnSync, log = console.log } = {}) {
|
|
362
|
+
const target = platformTarget(); requireBuilt(target); home = checkPath(home);
|
|
363
|
+
const selectedBundle = resolveBundle(options, target);
|
|
364
|
+
checkPath(home);
|
|
365
|
+
for (const legacy of ['active.json', 'receipts', 'runtimes']) {
|
|
366
|
+
const legacyPath = path.join(home, legacy); checkPath(legacyPath);
|
|
367
|
+
if (exists(legacyPath)) throw new Error('This directory contains an existing non-native installation; choose a separate EmilyRemoteNative home');
|
|
368
|
+
}
|
|
369
|
+
const base = path.join(home, 'native'); checkPath(base);
|
|
370
|
+
for (const leaf of ['active.json', '.install-lock', 'releases']) checkPath(path.join(base, leaf));
|
|
371
|
+
ensureDir(base); const lock = path.join(base, '.install-lock');
|
|
372
|
+
try { fs.mkdirSync(lock, { mode: 0o700 }); } catch (error) { if (error.code === 'EEXIST') throw new Error(`A native installer is running or was interrupted: inspect ${lock}; no lock was removed`); throw error; }
|
|
373
|
+
const token = crypto.randomBytes(16).toString('hex'); let attempt, activeChanged = false, oldActive;
|
|
374
|
+
try {
|
|
375
|
+
writeJson(path.join(lock, 'owner.json'), { pid: process.pid, token, started_at: new Date().toISOString() });
|
|
376
|
+
const activePath = path.join(base, 'active.json'); oldActive = optionalBytes(activePath);
|
|
377
|
+
if (oldActive !== null) verifyInstalled(home);
|
|
378
|
+
attempt = path.join(base, 'releases', crypto.randomUUID()); ensureDir(attempt);
|
|
379
|
+
const snapshot = snapshotBundle(selectedBundle.bundle, path.join(attempt, 'bundle.tar.gz'), selectedBundle.sha256);
|
|
380
|
+
const entries = parseTar(fs.readFileSync(snapshot.path)); const verified = verifyManifest(entries, target);
|
|
381
|
+
const runtime = path.join(attempt, 'runtime'); extract(entries, runtime);
|
|
382
|
+
const self_check = selfCheck(runtime, runner);
|
|
383
|
+
const reverified = verifyManifest(installedEntries(runtime), target);
|
|
384
|
+
if (reverified.manifest_sha256 !== verified.manifest_sha256) throw new Error('Runtime changed its manifest during self-check');
|
|
385
|
+
const active = { schema: 'emilyremote.native-installation/1', state: 'READY', version: VERSION, target,
|
|
386
|
+
runtime_relative: path.relative(home, runtime), bundle_sha256: snapshot.sha256, bundle_bytes: snapshot.bytes,
|
|
387
|
+
manifest_sha256: verified.manifest_sha256, build_manifest_sha256: verified.build_manifest_sha256,
|
|
388
|
+
installed_at: new Date().toISOString(), self_check,
|
|
389
|
+
trust: options.bundle !== undefined ? 'Operator-supplied archive SHA-256; no publisher signature was verified' : 'Bundled archive SHA-256 pinned in the npm installer; no separate publisher signature was verified',
|
|
390
|
+
bundle_source: options.bundle !== undefined ? 'operator-supplied' : 'npm-bundled', service_started: false, enrolled: false };
|
|
391
|
+
writeJson(path.join(attempt, 'receipt.json'), active);
|
|
392
|
+
activeChanged = true; writeJson(activePath, active);
|
|
393
|
+
log(JSON.stringify({ state: 'READY', version: VERSION, runtime, bundle_sha256: active.bundle_sha256, active: activePath }));
|
|
229
394
|
return 0;
|
|
230
395
|
} catch (error) {
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
checkHomePath(home, failure);
|
|
235
|
-
writeJsonAtomic(failure, { state: 'FAILED', version: VERSION, runtime_relative: path.relative(home, runtime),
|
|
236
|
-
failed_at: new Date().toISOString(), error: error.message,
|
|
237
|
-
note: 'This attempt was not published as a READY installation. Partial files retained for diagnosis.' });
|
|
238
|
-
} catch (_) { /* Preserve the original error; never write through an unsafe path. */ }
|
|
239
|
-
}
|
|
396
|
+
if (activeChanged) restore(path.join(base, 'active.json'), oldActive);
|
|
397
|
+
if (attempt) { try { writeJson(path.join(attempt, 'failure.json'), { state: 'FAILED', error: error.message, previous_active_preserved: oldActive !== null, at: new Date().toISOString() }); } catch { /* Preserve original failure. */ } }
|
|
240
398
|
throw error;
|
|
241
399
|
} finally {
|
|
242
|
-
|
|
400
|
+
const ownerPath = path.join(lock, 'owner.json'); checkPath(ownerPath);
|
|
401
|
+
if (exists(ownerPath)) { const owner = JSON.parse(fs.readFileSync(ownerPath)); if (owner.token !== token) throw new Error('Native installer lock ownership changed; not removing it'); fs.unlinkSync(ownerPath); }
|
|
402
|
+
fs.rmdirSync(lock); syncDir(base);
|
|
243
403
|
}
|
|
244
404
|
}
|
|
245
|
-
|
|
246
|
-
|
|
405
|
+
function parseArgs(argv) {
|
|
406
|
+
const result = { args: [] };
|
|
407
|
+
for (let index = 0; index < argv.length; index++) {
|
|
408
|
+
if (argv[index] === '--') { result.args.push(...argv.slice(index + 1)); break; }
|
|
409
|
+
if (['--home', '--bundle', '--sha256'].includes(argv[index])) {
|
|
410
|
+
const key = argv[index].slice(2); if (!argv[index + 1] || argv[index + 1].startsWith('--') || result[key] !== undefined) throw new Error(`Invalid or duplicate --${key}`);
|
|
411
|
+
result[key] = argv[++index];
|
|
412
|
+
} else result.args.push(argv[index]);
|
|
413
|
+
}
|
|
414
|
+
result.command = result.args.shift();
|
|
415
|
+
if ((result.bundle || result.sha256) && result.command !== 'setup') throw new Error('--bundle and --sha256 are setup-only options');
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
function delegate(executable, args) {
|
|
247
419
|
return new Promise((resolve, reject) => {
|
|
248
|
-
const child =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
process.on('SIGINT', onInt); process.on('SIGTERM', onTerm);
|
|
254
|
-
const cleanup = () => { process.off('SIGINT', onInt); process.off('SIGTERM', onTerm); };
|
|
255
|
-
child.once('error', (error) => { cleanup(); reject(new Error(`Unable to launch installed SDK: ${error.message}`)); });
|
|
420
|
+
const child = spawn(executable, args, { shell: false, stdio: 'inherit', env: cleanEnv(), windowsHide: true });
|
|
421
|
+
const interrupt = () => child.kill('SIGINT'); const terminate = () => child.kill('SIGTERM');
|
|
422
|
+
process.on('SIGINT', interrupt); process.on('SIGTERM', terminate);
|
|
423
|
+
const cleanup = () => { process.off('SIGINT', interrupt); process.off('SIGTERM', terminate); };
|
|
424
|
+
child.once('error', (error) => { cleanup(); reject(error); });
|
|
256
425
|
child.once('exit', (code, signal) => { cleanup(); resolve(code === null ? signal === 'SIGINT' ? 130 : 143 : code); });
|
|
257
426
|
});
|
|
258
427
|
}
|
|
259
|
-
|
|
260
428
|
async function main(argv = process.argv.slice(2)) {
|
|
429
|
+
if (Number(process.versions.node.split('.')[0]) < 22) throw new Error('This native installer requires Node.js 22 or newer; no runtimes were changed');
|
|
261
430
|
const parsed = parseArgs(argv);
|
|
262
431
|
if (!parsed.command || ['--help', '-h', 'help'].includes(parsed.command)) { console.log(HELP); return 0; }
|
|
263
432
|
if (parsed.command === '--version') { console.log(VERSION); return 0; }
|
|
433
|
+
if (parsed.command === 'platforms') { console.log(JSON.stringify(BUILDS, null, 2)); return 0; }
|
|
264
434
|
const home = path.resolve(parsed.home || defaultHome());
|
|
265
|
-
if (parsed.command === 'setup') {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
const installed = readReceipt(home);
|
|
271
|
-
const verification = verifyInstalled(installed.runtime);
|
|
272
|
-
if (parsed.command === 'status') {
|
|
273
|
-
if (parsed.args.length) throw new Error('status does not accept SDK arguments');
|
|
274
|
-
console.log(JSON.stringify({ state: 'READY', version: VERSION, runtime: installed.runtime,
|
|
275
|
-
receipt: receiptPath(home), verification, service_started_by_wrapper: false }, null, 2));
|
|
276
|
-
return 0;
|
|
277
|
-
}
|
|
278
|
-
return delegate(installed.runtime, [parsed.command, ...parsed.args]);
|
|
435
|
+
if (parsed.command === 'setup') { if (parsed.args.length) throw new Error('Unknown setup arguments; use --help'); return setup(home, parsed); }
|
|
436
|
+
const installed = verifyInstalled(home);
|
|
437
|
+
if (parsed.command === 'status') { console.log(JSON.stringify({ ...installed.active, installer_version: VERSION, runtime: installed.root }, null, 2)); return 0; }
|
|
438
|
+
return delegate(path.join(installed.root, installed.manifest.entrypoint), [parsed.command, ...parsed.args]);
|
|
279
439
|
}
|
|
280
|
-
|
|
281
|
-
module.exports = { main, parseArgs, defaultHome, cleanEnv, checkHomeAncestors, checkHomePath, runChecked, discoverPython, readReceipt,
|
|
282
|
-
verifyInstalled, setup, delegate, pythonPath, VERSION, WHEEL_SHA, WHEEL, receiptPath };
|
|
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,13 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robsonai/emilyremote",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "RobsonAI EmilyRemote compiled device runtime for Linux x64 and Apple Silicon macOS",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
-
"engines": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"emilyremote": "bin/emilyremote-native.cjs",
|
|
11
|
+
"emilyremote-native": "bin/emilyremote-native.cjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/emilyremote-native.cjs",
|
|
15
|
+
"lib/cli.cjs",
|
|
16
|
+
"README.md",
|
|
17
|
+
"RUNTIME_FORMAT.md",
|
|
18
|
+
"vendor/robsonai-emilyremote-native-0.4.1-linux-x64.tar.gz",
|
|
19
|
+
"vendor/robsonai-emilyremote-native-0.4.1-darwin-arm64.tar.gz"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test test/*.test.cjs",
|
|
23
|
+
"check-release": "node validation/check-release.cjs"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"tag": "latest"
|
|
28
|
+
},
|
|
29
|
+
"author": "RobsonAI",
|
|
30
|
+
"os": [
|
|
31
|
+
"linux",
|
|
32
|
+
"darwin"
|
|
33
|
+
],
|
|
34
|
+
"cpu": [
|
|
35
|
+
"x64",
|
|
36
|
+
"arm64"
|
|
37
|
+
]
|
|
13
38
|
}
|
|
Binary file
|
|
Binary file
|
package/PROVENANCE.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schema": "emilyremote.npm-provenance/1",
|
|
3
|
-
"display_name": "EmilyRemote",
|
|
4
|
-
"npm_package_name": "@robsonai/emilyremote",
|
|
5
|
-
"npm_name_status": "requested organization target; robsonai organization ownership and publication are pending verification",
|
|
6
|
-
"primary_command": "emilyremote",
|
|
7
|
-
"prior_distribution": {
|
|
8
|
-
"name": "@martinrobson/dialhome@0.2.0",
|
|
9
|
-
"status": "published; anonymous version metadata and tarball verified",
|
|
10
|
-
"tarball_sha256": "dfd56b3bd0f8e57b07559bf43551c05c58957732cd69e70c37a5fbbf86506326"
|
|
11
|
-
},
|
|
12
|
-
"sdk_version": "0.3.0",
|
|
13
|
-
"wheel": "vendor/robsonai_emilyremote-0.3.0-py3-none-any.whl",
|
|
14
|
-
"wheel_sha256": "e730b011b844dc70442fa793d29c98eb9df8dea5341ebbb49ba38fd73da0269e",
|
|
15
|
-
"source_changes": "Version 0.3.0 uses the emilyremote Python namespace and robsonai-emilyremote distribution, with release security fixes. Prior release test results do not certify this build.",
|
|
16
|
-
"scope": "Host Python runtime only; optional dependencies, Android APK, and microcontroller firmware are not installed by this npm wrapper",
|
|
17
|
-
"sdk_source": "RobsonAI EmilyRemote 0.3.0 release source, rebuilt after namespace and security changes"
|
|
18
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""Read-only installed-wheel verification. Invoked with isolated Python -I -B."""
|
|
2
|
-
import argparse
|
|
3
|
-
import hashlib
|
|
4
|
-
import importlib.metadata
|
|
5
|
-
import importlib.util
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
import sys
|
|
10
|
-
import zipfile
|
|
11
|
-
|
|
12
|
-
EXPECTED_SHA = "e730b011b844dc70442fa793d29c98eb9df8dea5341ebbb49ba38fd73da0269e"
|
|
13
|
-
parser = argparse.ArgumentParser()
|
|
14
|
-
parser.add_argument("--wheel", required=True)
|
|
15
|
-
parser.add_argument("--runtime", required=True)
|
|
16
|
-
args = parser.parse_args()
|
|
17
|
-
assert sys.version_info >= (3, 11), "Python 3.11+ is required"
|
|
18
|
-
wheel = Path(args.wheel)
|
|
19
|
-
assert hashlib.sha256(wheel.read_bytes()).hexdigest() == EXPECTED_SHA, "Bundled wheel mismatch"
|
|
20
|
-
runtime = Path(args.runtime).resolve()
|
|
21
|
-
assert Path(sys.prefix).resolve() == runtime, "Python is not running in the selected virtualenv"
|
|
22
|
-
assert sys.prefix != sys.base_prefix, "Global Python is not an isolated runtime"
|
|
23
|
-
assert importlib.metadata.version("robsonai-emilyremote") == "0.3.0", "Installed metadata version mismatch"
|
|
24
|
-
spec = importlib.util.find_spec("emilyremote")
|
|
25
|
-
assert spec and spec.origin and spec.origin.endswith("__init__.py"), "SDK did not resolve to Python source"
|
|
26
|
-
package = Path(spec.origin).resolve().parent
|
|
27
|
-
assert runtime in package.parents, "SDK import came from outside its virtualenv"
|
|
28
|
-
hashes = {}
|
|
29
|
-
with zipfile.ZipFile(wheel) as archive:
|
|
30
|
-
for name in archive.namelist():
|
|
31
|
-
if not name.startswith("emilyremote/") or not name.endswith(".py"):
|
|
32
|
-
continue
|
|
33
|
-
relative = name[len("emilyremote/"):]
|
|
34
|
-
actual = package / relative
|
|
35
|
-
assert package in actual.resolve().parents, "Source path escapes installed SDK"
|
|
36
|
-
expected = hashlib.sha256(archive.read(name)).hexdigest()
|
|
37
|
-
assert hashlib.sha256(actual.read_bytes()).hexdigest() == expected, "Installed source mismatch: " + name
|
|
38
|
-
hashes[name] = expected
|
|
39
|
-
actual_names = {"emilyremote/" + p.relative_to(package).as_posix() for p in package.rglob("*.py")}
|
|
40
|
-
assert actual_names == set(hashes), "Installed SDK source set differs from the wheel"
|
|
41
|
-
all_files = {"emilyremote/" + p.relative_to(package).as_posix() for p in package.rglob("*") if p.is_file()}
|
|
42
|
-
assert all_files == set(hashes), "Unexpected installed SDK files (including bytecode or native modules)"
|
|
43
|
-
import emilyremote
|
|
44
|
-
assert emilyremote.__version__ == "0.3.0", "Runtime version mismatch"
|
|
45
|
-
print(json.dumps({"ok": True, "version": emilyremote.__version__, "python": sys.version.split()[0],
|
|
46
|
-
"executable": sys.executable, "runtime": str(runtime), "package": str(package),
|
|
47
|
-
"wheel_sha256": EXPECTED_SHA, "source_hashes": hashes,
|
|
48
|
-
"uid": os.getuid() if hasattr(os, "getuid") else None}, sort_keys=True))
|
|
Binary file
|