dshm-compat 0.1.3 → 0.1.4

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/AGENTS.md ADDED
@@ -0,0 +1,17 @@
1
+ # dshm-compat development rules
2
+
3
+ ## No per-plugin adaptation
4
+
5
+ - **Never** add a production branch, recipe, rule, or hand-built artifact for one
6
+ package name. `dshm-compat` must adapt plugins through its generic engines.
7
+ - S2/native plugins go through `lib/s2-native.js`: detect the worker and the
8
+ standard runtime/spawn patterns, stage the worker, normalize the manifest, and
9
+ validate the result. Package names may appear only in tests and documentation
10
+ examples, never in `if (name === ...)` logic.
11
+ - If a plugin has a genuinely unique protocol, expose a **generic extension
12
+ point** in the plugin engine (patterns, hooks, or sidecar descriptor), not a
13
+ one-off patch.
14
+ - `dsh-ops` is a regression fixture. It proves the generic S2 adapter; it is not
15
+ a special case.
16
+ - Deliverables are the plugin and its generic tooling, not a converted tarball
17
+ for a single plugin.
package/CHANGELOG.md CHANGED
@@ -1,16 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ - The generic S2 adapter now accepts **x86_64** workers: it bundles
6
+ `qemu-x86_64` and a minimal x86_64 glibc rootfs, stages a shell wrapper, and
7
+ runs the worker under QEMU in the guest. No AArch64 rebuild is required for
8
+ plugins that publish a `linux-x64` runtime.
9
+ - If the package contains no worker, the adapter looks at the GitHub repository
10
+ release assets, prefers `linux-arm64` then `linux-x64`, extracts the archive
11
+ (ZIP or tar.gz), and uses the first AArch64/x86_64 ELF. No package name is
12
+ special-cased.
13
+ - Source-only native packages (for example a Rust `native/` tree with no
14
+ prebuilt runtime) now classify as S2 instead of S0, so the native adapter gets
15
+ a chance to build or acquire a worker.
16
+ - Verified end-to-end: `compat_convert("https://github.com/T-Auto/dsh-ops")`
17
+ with **no workerPath** acquires the upstream `linux-x64` release asset, wraps
18
+ it in QEMU, patches the runtime/spawn/peer/os-cpu gates, and validates.
19
+ - Offline regression suite: 27/27.
20
+
3
21
  ## 0.1.3
4
22
 
5
- - Fixed dsh-ops recipe dispatch for GitHub specs: the converter now keys on
6
- `report.package.name`, not `report.resolved.name` (which is `owner/repo` for
7
- GitHub). `compat_convert("https://github.com/T-Auto/dsh-ops", ...)` now
8
- reaches the recipe and returns `needs-native-build` instead of `skipped`.
9
- - The dsh-ops recipe relaxes `peerDependencies["@deepseek-ai/dsh"]` to
10
- `workspace:*` so installing it does not make the DSHM core fail its
11
- prerelease compatibility check on the next boot.
12
- - Clearer S2 skipped message ("needs a recipe or an external native build").
13
- - Offline regression suite: 24/24.
23
+ - Added a **generic S2 native adapter** (`lib/s2-native.js`). It discovers the
24
+ worker and the runtime matrix in the package, appends the DSHM target,
25
+ rewrites `${platform}-${arch}` templates, wraps standard `const/let argv =
26
+ [file]` spawn calls in `bash -c`, removes foreign `os`/`cpu` gates and
27
+ non-Linux optional payloads, and relaxes the DSH `peerDependencies` range.
28
+ There are no package-name branches and no built-in per-package rules.
29
+ - Added a generic guest sidecar bridge (`compat_sidecar` / `lib/sidecar.js`):
30
+ stage a Linux/AArch64 worker from a converted tarball into
31
+ `$DSH_HOME/dshm-compat/sidecars/<id>`, list/probe it, and call it with one
32
+ JSON request/response through the Alpine guest.
33
+ - `compat_plan` now infers `rust`, `c/c++`, or `node-gyp` from source files in
34
+ the report instead of relying on a package-specific rule.
35
+ - S2 conversion dispatches on the strategy and the generic patterns, not on a
36
+ package name; GitHub specs work through the same path.
37
+ - Offline regression suite: 25/25.
14
38
 
15
39
  ## 0.1.2
16
40
 
package/INSTALL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Installing dshm-compat
2
2
 
3
- The artifact is a normal DSH bundle package: `dshm-compat-0.1.3.tgz`.
4
- It is host-only, has no runtime dependencies, and adds eight agent tools.
3
+ The artifact is a normal DSH bundle package: `dshm-compat-0.1.4.tgz`.
4
+ It is host-only, has no runtime dependencies, and adds ten agent tools.
5
5
 
6
6
  ## A. DSHM (the tablet)
7
7
 
@@ -15,7 +15,7 @@ input calls `pnpm view` before installing and fails with
15
15
  `Command failed with EACCES: pnpm view ... spawn pnpm EACCES`. Use one of these
16
16
  instead:
17
17
 
18
- - ask the DSHM Agent to run `pnpm add dshm-compat@0.1.3` in its bash tool (the
18
+ - ask the DSHM Agent to run `pnpm add dshm-compat@0.1.4` in its bash tool (the
19
19
  bundled pnpm shim routes that command to the host install queue); or
20
20
  - install from a GitHub repository URL; or
21
21
  - after any working install, use the plugin's own `compat_install` tool for
@@ -28,7 +28,7 @@ not accept local paths, `file:` or `http://` specs.
28
28
 
29
29
  ### A2. HTTPS tarball
30
30
 
31
- Put `dshm-compat-0.1.3.tgz` on any HTTPS host (GitHub Release, Gitee Release,
31
+ Put `dshm-compat-0.1.4.tgz` on any HTTPS host (GitHub Release, Gitee Release,
32
32
  object storage) and paste the `.tgz` URL into the same Add plugin input. DSHM's
33
33
  installer accepts tarball specs; if the current market UI refuses a plain URL,
34
34
  use A1.
@@ -38,7 +38,7 @@ use A1.
38
38
  If `hdc` is available and the app can read the path:
39
39
 
40
40
  ```sh
41
- hdc file send dshm-compat-0.1.3.tgz /data/storage/el2/base/haps/entry/files/dshm-compat-0.1.3.tgz
41
+ hdc file send dshm-compat-0.1.4.tgz /data/storage/el2/base/haps/entry/files/dshm-compat-0.1.4.tgz
42
42
  ```
43
43
 
44
44
  Then enter the absolute path (or `file:/...`) in the Add plugin input. This
@@ -47,7 +47,7 @@ depends on the DSHM build's local-file support; A1 is safer.
47
47
  ## B. Desktop DSH (for testing only)
48
48
 
49
49
  ```powershell
50
- & 'D:\安装\dsh-desktop\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'D:\dsh\dsh-26-09-30\dshm-compat\dshm-compat-0.1.3.tgz'
50
+ & 'D:\安装\dsh-desktop\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'D:\dsh\dsh-26-09-30\dshm-compat\dshm-compat-0.1.4.tgz'
51
51
  ```
52
52
 
53
53
  If the profile reports `ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`, its `node_modules`
package/README.md CHANGED
@@ -5,11 +5,12 @@ them through the least invasive strategy that can work.
5
5
 
6
6
  ## Current phase
7
7
 
8
- 0.1.3 fixes dsh-ops recipe dispatch for GitHub specs and relaxes its DSH peer
9
- range; it also adds `compat_doctor`, rollback-safe local installs, and codeload
10
- fallbacks for GitHub. Phase 1 ships the analyzer, the S0/S1 converter, the
11
- dsh-ops recipe, guest probing/provisioning, the DSHM install-queue transport,
12
- and a profile-local fallback for artifacts the host queue rejects:
8
+ 0.1.4 adds a generic S2 native adapter (including an x86_64 worker + bundled
9
+ QEMU fallback and GitHub Release worker acquisition), `compat_doctor`,
10
+ rollback-safe local installs, and codeload fallbacks for GitHub. Phase 1 ships
11
+ the analyzer, the S0/S1 converter, the generic S2 engine, guest
12
+ probing/provisioning, the DSHM install-queue transport, and a profile-local
13
+ fallback for artifacts the host queue rejects:
13
14
 
14
15
  - `compat_analyze` downloads an npm/GitHub/local package and classifies it.
15
16
  - `compat_plan` returns the concrete conversion plan without changing anything.
@@ -18,12 +19,19 @@ and a profile-local fallback for artifacts the host queue rejects:
18
19
  install lifecycle scripts, client platform), vendors missing pure-JS runtime
19
20
  dependencies into a flat `node_modules`, validates the resulting tarball, and
20
21
  repacks it as a DSHM tarball; for S1 it removes a native dependency that the
21
- DSHM core already provides. For `dsh-ops` it applies the 0.3.x recipe once an
22
- AArch64 Linux worker is supplied via `workerPath`; without one it returns
23
- `needs-native-build` with the exact cross-build command.
22
+ DSHM core already provides. For S2 it uses the generic native adapter: it
23
+ detects the worker, `PLATFORMS` matrix, `${platform}-${arch}` templates,
24
+ standard spawn calls, and foreign `os`/`cpu`/peer gates. A worker can come from
25
+ `workerPath`, the package's `runtime/linux-*`, or the GitHub Release assets
26
+ (linux-arm64 first, then linux-x64). An x86_64 worker is executed through the
27
+ bundled `qemu-x86_64` and glibc rootfs, so no AArch64 rebuild is required.
28
+ Only when none of these exist does it return `needs-native-build` with the
29
+ cross-build command. Package names are not special-cased.
24
30
  - `compat_guest_probe` reports the Alpine guest kernel/packages/tools.
25
31
  - `compat_guest_provision` installs a reviewed allowlist of Alpine packages in
26
32
  the guest (`confirm:true` required).
33
+ - `compat_sidecar` stages a native Linux/AArch64 worker from a converted
34
+ tarball and lists/probes/calls it in the guest with JSON stdin/stdout.
27
35
  - `compat_status` lists stored reports and install attempts.
28
36
  - `compat_doctor` checks DSHM mode, core, profile/bundle, queue, state, and the
29
37
  guest in one read-only call.
@@ -41,16 +49,24 @@ conversion.
41
49
 
42
50
  See [architecture](docs/ARCHITECTURE.md) and [conversion recipes](docs/RECIPES.md).
43
51
 
52
+ ## Development policy
53
+
54
+ There are **no per-plugin production branches**. S2 native plugins go through
55
+ the generic `lib/s2-native.js` engine; package names appear only in tests and
56
+ documentation examples. If a plugin has a unique protocol, add a generic
57
+ extension point (pattern, hook, or sidecar descriptor) instead of a one-off
58
+ patch. See [AGENTS.md](AGENTS.md).
59
+
44
60
  ## CLI
45
61
 
46
62
  The same converter runs on a PC without installing the DSH plugin:
47
63
 
48
64
  ```sh
49
65
  node bin/dshm-compat.mjs analyze dsh-m
50
- node bin/dshm-compat.mjs plan dsh-ops
66
+ node bin/dshm-compat.mjs plan some-native-plugin
51
67
  node bin/dshm-compat.mjs convert dsh-cache-billing --no-vendor
52
- node bin/dshm-compat.mjs convert dsh-ops --worker ./dsh-ops-worker
53
- node bin/dshm-compat.mjs scan dsh-m dsh-ops some-plugin
68
+ node bin/dshm-compat.mjs convert some-native-plugin --worker ./some-native-worker
69
+ node bin/dshm-compat.mjs scan dsh-m some-native-plugin some-plugin
54
70
  ```
55
71
 
56
72
  `convert` writes the tarball under `$DSH_HOME/dshm-compat/builds` (override with
@@ -0,0 +1,17 @@
1
+ # Bundled QEMU x86_64 user-mode runtime
2
+
3
+ This directory is a generic fallback for S2 plugins that ship a `linux-x64`
4
+ native worker but no AArch64 build. `qemu-x86_64` executes the x86_64 worker in
5
+ the DSHM Alpine guest; the `rootfs/` directory supplies the glibc loader and
6
+ libraries the worker links against.
7
+
8
+ - `qemu-x86_64`: Alpine 3.22 `qemu-x86_64` 10.0.0-r1 (aarch64 build), GPL-2.0.
9
+ Source: https://pkgs.alpinelinux.org/package/v3.22/community/aarch64/qemu-x86_64
10
+ - `rootfs/lib64/ld-linux-x86-64.so.2`,
11
+ `rootfs/lib/x86_64-linux-gnu/libc.so.6`,
12
+ `rootfs/lib/x86_64-linux-gnu/libgcc_s.so.1`: Ubuntu 24.04 x86_64 base rootfs,
13
+ LGPL-2.1/GPL-2.0 with GCC runtime exception.
14
+ Source: https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/
15
+
16
+ The fallback is optional: when a plugin has a native AArch64 worker, the generic
17
+ S2 adapter stages that directly and these files are not used.
Binary file
@@ -37,7 +37,7 @@ spec ──► resolve (npm / GitHub / local .tgz)
37
37
  ──► extract (ustar/pax, no dependency)
38
38
  ──► analyze (manifest, native files, scripts, platform/Electron scan, core inventory)
39
39
  ──► plan (S0..S4 + concrete steps)
40
- ──► convert (S0/S1 now; S2+ is a plan)
40
+ ──► convert (S0/S1 manifest, S2 generic native adapter)
41
41
  ──► pack (ustar writer)
42
42
  ──► install queue (DSHM only)
43
43
  ```
@@ -50,14 +50,18 @@ spec ──► resolve (npm / GitHub / local .tgz)
50
50
  | `lib/tar.js` | ustar/pax extraction with traversal and size guards |
51
51
  | `lib/analyze.js` | classification evidence, strategy, confidence, next action |
52
52
  | `lib/core.js` | locate `$DSH_HOME/cores/<version>/node_modules` and compare runtime dependencies |
53
- | `lib/rules.js` | exact recipes for known packages (`dsh-ops`) |
54
- | `lib/convert.js` | S0 manifest rewrite/repack and vendoring; S1 native-dependency removal |
53
+ | `lib/rules.js` | operator-supplied custom rules only; no built-in per-package rules |
54
+ | `lib/convert.js` | S0 manifest rewrite/repack and vendoring; S1 native-dependency removal; dispatch to the S2 adapter |
55
+ | `lib/s2-native.js` | generic S2 worker/runtime/spawn/peer adapter |
56
+ | `lib/release.js` | GitHub Release Linux worker discovery and extraction |
57
+ | `lib/zip.js` | dependency-free ZIP reader for release assets |
55
58
  | `lib/vendor.js` | flat `node_modules` vendoring for missing pure-JS dependencies |
56
59
  | `lib/validate.js` | post-conversion manifest/entry/dependency validation |
57
60
  | `lib/plan.js` | user-visible conversion plan for every strategy |
58
61
  | `lib/guest.js` | read-only Alpine guest probe through `subprocess` |
59
62
  | `lib/install.js` | DSHM install-queue producer with polling and cancellation |
60
63
  | `lib/local-install.js` | profile-local tarball install with staging, backup, and rollback |
64
+ | `lib/sidecar.js` | generic guest worker staging and JSON-stdio calls |
61
65
  | `lib/doctor.js` | one-shot DSHM/core/profile/queue/guest diagnosis |
62
66
  | `lib/state.js` | bounded report/install index and atomic writes |
63
67
  | `lib/pack.js` | dependency-free ustar/gzip writer |
@@ -72,6 +76,7 @@ spec ──► resolve (npm / GitHub / local .tgz)
72
76
  | `compat_convert` | writes a converted tarball; optionally submits it to the install queue |
73
77
  | `compat_guest_probe` | read-only guest command |
74
78
  | `compat_guest_provision` | installs allowlisted Alpine packages in the guest |
79
+ | `compat_sidecar` | stages/lists/probes/calls a JSON-stdio native worker in the guest |
75
80
  | `compat_doctor` | read-only DSHM/core/profile/queue/guest diagnosis |
76
81
  | `compat_install` | writes one install-queue request; runs third-party code |
77
82
  | `compat_status` | read-only |
@@ -79,11 +84,12 @@ spec ──► resolve (npm / GitHub / local .tgz)
79
84
  ## Phase status
80
85
 
81
86
  - **Phase 1 (done):** analyzer, batch triage, S0 manifest converter + flat
82
- dependency vendoring + output validation, S1 manifest substitution,
83
- dependency preflight, guest probe/provisioning, worker execution primitive,
84
- dsh-ops 0.3.x recipe (external AArch64 worker), install queue, offline tests.
85
- - **Phase 2 (next):** on-device guest verification, dsh-ops worker build, nested
86
- version-conflict vendoring, sidecar RPC.
87
+ dependency vendoring + output validation, S1 manifest substitution, generic S2
88
+ native adapter (AArch64 worker, x86_64 worker through bundled QEMU, GitHub
89
+ Release acquisition), dependency preflight, guest probe/provisioning, worker
90
+ execution primitive, install queue, offline tests.
91
+ - **Phase 2 (next):** on-device guest verification, automatic worker build
92
+ (desktop/CI), nested version-conflict vendoring, sidecar RPC.
87
93
  - **Phase 3:** WASM/remote fallback, market UI entry, compatibility registry
88
94
  updates.
89
95
 
package/docs/RECIPES.md CHANGED
@@ -1,104 +1,73 @@
1
- # Conversion recipes
1
+ # Generic conversion patterns
2
2
 
3
- These are the concrete recipes behind `S1`/`S2` plans. They are intentionally
4
- written as patches to the upstream package, not as claims that the current
5
- converter executes them.
3
+ These are the patterns behind `S1`/`S2` plans. They are **generic**: no
4
+ production branch is keyed on a package name. A package name may appear only in
5
+ tests or as a documentation example.
6
6
 
7
- ## `dsh-ops` (Rust native worker)
7
+ ## S2: native worker
8
8
 
9
- Upstream: `T-Auto/dsh-ops`. The core is `native/` (Rust) and the published
10
- worker is `runtime/<target>/dsh-ops-worker`.
9
+ The generic adapter in `lib/s2-native.js` recognizes the common shape of a
10
+ native DSH plugin:
11
11
 
12
- ### 1. Support matrix
12
+ 1. A worker binary under `runtime/<platform>-<arch>/<name>`, a worker passed
13
+ explicitly with `workerPath`, or a Linux asset on the repository's GitHub
14
+ Release (arm64 preferred, then x64).
15
+ 2. A JS module exporting a `PLATFORMS` array and interpolating
16
+ `${platform}-${arch}` into runtime paths.
17
+ 3. Spawn calls of the form `const argv = [file]` or `let argv = [file]`.
18
+ 4. Optional `package.json` `os`/`cpu` gates and a DSH `peerDependencies` range.
13
19
 
14
- `lib/ptc/runtime.js` hard-codes:
20
+ For each match it:
15
21
 
16
- ```js
17
- const PLATFORMS = ['win32-x64', 'linux-x64', 'darwin-arm64']
18
- ```
19
-
20
- `resolveWorker()` checks this matrix **before** considering
21
- `DSH_OPS_WORKER_BIN`, so a binary path override alone cannot unblock DSHM.
22
-
23
- Patch:
22
+ - appends `linux-arm64` (AArch64 worker) or `linux-x64` (x86_64 worker) to
23
+ `PLATFORMS` and injects a `resolveTarget()` helper so `openharmony`/arm64
24
+ resolves to that target in both `resolveWorker` and `tryResolveWorker`;
25
+ - replaces `${platform}-${arch}` **inside template literals** with
26
+ `${resolveTarget(platform, arch)}`;
27
+ - wraps every standard spawn call in
28
+ `['bash', '-c', 'exec "$0" "$@"', file]`, which `dshm-vm-bash` routes to the
29
+ Alpine guest;
30
+ - removes `os`/`cpu`, drops win32/darwin optional payloads, and relaxes
31
+ `@deepseek-ai/dsh` to `workspace:*`;
32
+ - adds `openharmony`/target/arch to `dsh-plugin.json` component targets;
33
+ - stages the worker at `runtime/<target>/<worker>` with a matching
34
+ `provenance.json` (`version`, `target`, `executable`, `sha256`).
24
35
 
25
- - add `linux-arm64` to `PLATFORMS`;
26
- - map an OHOS `process.platform` to `linux-arm64` before the matrix check;
27
- - keep `workerName()` returning `dsh-ops-worker` on non-win32;
28
- - look for `runtime/linux-arm64/dsh-ops-worker`.
36
+ An x86_64 worker is staged as a shell wrapper plus `qemu-x86_64`, a minimal
37
+ x86_64 glibc rootfs, and the original `<worker>-x64`; the wrapper runs QEMU in
38
+ the guest. This makes a `linux-x64` runtime usable without an AArch64 rebuild.
29
39
 
30
- Also update:
40
+ If no worker is supplied, none is found in the package, and the GitHub Release
41
+ has no Linux asset, the result is `needs-native-build` with the cross-build
42
+ command. The adapter never builds the worker itself.
31
43
 
32
- - `package.json` `os`/`cpu` (remove the gates or add `openharmony`/`arm64`);
33
- - `dsh-plugin.json` `platforms`, `architectures`, and `targets` for each
34
- `x-components` entry.
44
+ ### Building the worker
35
45
 
36
- ### 2. Build the worker
37
-
38
- From a Linux build host or CI:
46
+ For a Rust crate under `native/`:
39
47
 
40
48
  ```sh
41
- rustup target add aarch64-unknown-linux-musl
42
- # either
43
- cross build --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
44
- # or
49
+ cargo build --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
50
+ # or, with a Zig-based cross linker:
45
51
  cargo zigbuild --release --locked --manifest-path native/Cargo.toml --target aarch64-unknown-linux-musl
46
52
  ```
47
53
 
48
- The Windows/macOS metadata crates are target-gated in `native/Cargo.toml`; the
49
- portable dependencies are `cap-std`, `cap-fs-ext`, `fs2`, `rustix`, `sha2`,
50
- `serde`, `serde_json`, `uuid`, `regex`.
51
-
52
- Stage the result as `runtime/linux-arm64/dsh-ops-worker` with a matching
53
- `provenance.json` (`version`, `target`, `executable`, `sha256`).
54
+ The result must be a static AArch64 musl ELF: `e_machine == 183`, no
55
+ `PT_INTERP`. Dynamic glibc binaries will not start in the Alpine guest.
54
56
 
55
- ### 3. Spawn through the guest
57
+ ### Generic sidecar
56
58
 
57
- `lib/file/worker.js` and `lib/ptc/worker.js` call:
59
+ A native worker that follows one-JSON-request / one-JSON-response can also be
60
+ staged and called without patching the plugin's own tool layer:
58
61
 
59
- ```js
60
- subprocess.spawn({ argv: [file], ... })
62
+ ```json
63
+ compat_sidecar { "action": "stage", "tarball": "<converted.tgz>", "workerPath": "runtime/linux-arm64/worker", "id": "my-worker" }
64
+ compat_sidecar { "action": "probe", "id": "my-worker" }
65
+ compat_sidecar { "action": "call", "id": "my-worker", "input": { "ping": true }, "confirm": true }
61
66
  ```
62
67
 
63
- `dshm-vm-bash` only redirects `bash -c`, so patch both call sites to:
64
-
65
- ```js
66
- argv: ['bash', '-c', 'exec "$0"', file]
67
- ```
68
-
69
- The worker path is under the profile's 9p share, so the guest sees the same
70
- absolute path. If exec from 9p fails on a device, copy the worker to
71
- `/tmp/dsh-ops-worker` once (hash-keyed) and execute that copy.
72
-
73
- ### 4. Shell component
74
-
75
- `lib/shells/resolve.js` treats every non-win32 host as POSIX and probes the
76
- **host** for `/bin/bash`. DSHM's host has no bash. Patch the DSHM branch to
77
- report the guest's `/bin/bash` as available; `ops_bash` then spawns
78
- `bash -c`, which `dshm-vm-bash` routes into the guest.
79
-
80
- Disable the PowerShell rung: the pwsh payload is win32-only.
81
-
82
- ### 5. PTC vs file
83
-
84
- `lib/ptc/worker.js` calls `sandbox.confine()` for non-`danger-full-access`
85
- policies. If DSHM's sandbox provider cannot confine the worker, PTC tools fail
86
- closed. The `file` component uses policy checks but no `sandbox.confine`, so
87
- port `ops_*` file tools first and treat PTC as a later optimization.
88
-
89
- ### 6. Verification
90
-
91
- Run on the device:
92
-
93
- - `ops_stat` on a workspace file;
94
- - `ops_grep` over a small tree;
95
- - `ops_write_file` then `ops_stat` (check the new revision);
96
- - `ops_copy` / `ops_move` (9p rename/atomicity);
97
- - a failure case with a read-only policy.
98
-
99
- Watch for `cap-std`/`rustix` failures on 9p (`openat2`, `statx`, `renameat2`,
100
- `flock`). If those fail, the fallback is a JS reimplementation of the worker
101
- protocol rather than a guest port.
68
+ Staging checks the worker is an AArch64 ELF and puts it under
69
+ `$DSH_HOME/dshm-compat/sidecars/<id>/`. Calls run through
70
+ `bash -c 'exec "$0" "$@"'`, which `dshm-vm-bash` routes into Alpine.
102
71
 
103
72
  ## Common native dependencies
104
73
 
package/index.js CHANGED
@@ -15,6 +15,7 @@ import { probeGuest, provisionGuest } from './lib/guest.js'
15
15
  import { enqueueInstall } from './lib/install.js'
16
16
  import { installLocalTarball, isLocalSpec } from './lib/local-install.js'
17
17
  import { buildPlan } from './lib/plan.js'
18
+ import { callSidecar, listSidecars, probeSidecar, stageSidecar } from './lib/sidecar.js'
18
19
  import { recordInstall, recordReport, status } from './lib/state.js'
19
20
 
20
21
  export const name = 'dshm-compat'
@@ -152,7 +153,7 @@ export function apply(ctx) {
152
153
  profileDir: { type: 'string', description: 'Absolute DSHM profile directory for install:true. Omit to let the host choose.' },
153
154
  confirm: { type: 'boolean', description: 'Required when install is true. Confirms that installing the converted package runs third-party code.' },
154
155
  vendor: { type: 'boolean', description: 'Vendor missing pure-JS runtime dependencies into the tarball. Default true.' },
155
- workerPath: { type: 'string', description: 'Absolute path to a prebuilt AArch64 Linux worker for recipes that need one (currently dsh-ops). Omit to receive the build command instead.' },
156
+ workerPath: { type: 'string', description: 'Absolute path to a prebuilt AArch64 Linux worker for an S2 plugin. Omit to let the generic adapter return the build command.' },
156
157
  allowLocal: { type: 'boolean', description: 'When install is true, unpack the converted tarball into the profile instead of the host queue. Default true; set false to force the queue.' },
157
158
  },
158
159
  required: ['spec'],
@@ -232,6 +233,50 @@ export function apply(ctx) {
232
233
  },
233
234
  ), 'compat_guest_provision')
234
235
 
236
+ register(definition(
237
+ 'compat_sidecar',
238
+ 'Stage and call a native Linux/AArch64 worker in the DSHM Alpine guest. Actions: stage (extract a worker from a converted tarball into $DSH_HOME/dshm-compat/sidecars/<id>), list, probe (run --version), call (one JSON request on stdin, one JSON value on stdout; requires confirm:true). '
239
+ + 'This is the generic S2 bridge; the worker runs through bash -c and is routed to the guest by dshm-vm-bash.',
240
+ {
241
+ type: 'object',
242
+ properties: {
243
+ action: { type: 'string', enum: ['stage', 'list', 'probe', 'call'] },
244
+ id: { type: 'string', description: 'Sidecar id (letters, digits, dot, underscore, dash).' },
245
+ tarball: { type: 'string', description: 'Converted package tarball containing the worker.' },
246
+ workerPath: { type: 'string', description: 'Worker path inside the tarball, e.g. runtime/linux-arm64/<worker>.' },
247
+ input: { type: 'object', additionalProperties: true, description: 'JSON request for action=call. Default {}.' },
248
+ timeoutMs: { type: 'integer', description: 'Per-call timeout. Default 120000.' },
249
+ confirm: { type: 'boolean', description: 'Required for action=call: confirms that the worker runs native code in the guest.' },
250
+ },
251
+ required: ['action'],
252
+ additionalProperties: false,
253
+ },
254
+ async (args, exec) => {
255
+ try {
256
+ if (args?.action === 'stage') {
257
+ if (typeof args.tarball !== 'string' || typeof args.workerPath !== 'string') {
258
+ return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'stage requires tarball and workerPath' } }
259
+ }
260
+ const result = stageSidecar(args.tarball, args.workerPath, { id: args.id })
261
+ return result.ok ? { status: 'ok', ...result } : { status: 'error', ...result }
262
+ }
263
+ if (args?.action === 'list') return { status: 'ok', sidecars: listSidecars() }
264
+ if (args?.action === 'probe') {
265
+ if (typeof args.id !== 'string') return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'probe requires id' } }
266
+ return { status: 'ok', ...(await probeSidecar(ctx, args.id, { timeoutMs: args.timeoutMs, signal: exec?.signal })) }
267
+ }
268
+ if (args?.action === 'call') {
269
+ if (typeof args.id !== 'string') return { status: 'error', error: { code: 'INVALID_ARGUMENT', message: 'call requires id' } }
270
+ if (args.confirm !== true) return { status: 'confirmation-required', message: 'Re-run with confirm:true to execute the worker in the guest.' }
271
+ return { status: 'ok', ...(await callSidecar(ctx, args.id, args.input ?? {}, { timeoutMs: args.timeoutMs, signal: exec?.signal })) }
272
+ }
273
+ return { status: 'error', error: { code: 'INVALID_ACTION', message: `unknown sidecar action: ${String(args?.action)}` } }
274
+ } catch (error) {
275
+ return errorResult(error)
276
+ }
277
+ },
278
+ ), 'compat_sidecar')
279
+
235
280
  register(definition(
236
281
  'compat_install',
237
282
  'Install an already-built plugin artifact. Local .tgz paths (or file:/link: paths) are unpacked into the owning DSH profile and its bundle list, because DSHM\'s host queue rejects local specs; npm names and GitHub repositories still go through the host install queue. '
package/lib/analyze.js CHANGED
@@ -245,10 +245,11 @@ export async function analyze(spec, options = {}) {
245
245
  confidence = 'high'
246
246
  reasons.push('the archive carries only win32 payloads and no C/C++/Rust source to rebuild')
247
247
  next.push('ask the plugin author for source or a linux-arm64 build')
248
- } else if (nativeFiles.length > 0 || unknownNativeDeps.length > 0 || electronSurface) {
248
+ } else if (nativeFiles.length > 0 || unknownNativeDeps.length > 0 || electronSurface || hasNativeSource) {
249
249
  strategy = 'S2'
250
250
  confidence = 'medium'
251
251
  if (nativeFiles.length > 0) reasons.push(`${nativeFiles.length} native file(s) need a linux-arm64 runtime outside the DSHM host process`)
252
+ if (hasNativeSource && nativeFiles.length === 0) reasons.push('native source needs a Linux/guest build; no prebuilt runtime is present')
252
253
  if (unknownNativeDeps.length > 0) reasons.push(`unrecognized native dependencies: ${unknownNativeDeps.map(dep => dep.name).join(', ')}`)
253
254
  if (Object.keys(installScripts).length > 0) reasons.push(`install scripts: ${Object.keys(installScripts).join(', ')}`)
254
255
  if (electronSurface) reasons.push(`Electron API surface detected (${scan.electron} reference(s)); needs a shim or a sidecar UI`)
@@ -360,6 +361,10 @@ export async function analyze(spec, options = {}) {
360
361
  },
361
362
  nativeDeps,
362
363
  unknownNativeDeps: unknownNativeDeps.map(dep => dep.name),
364
+ nativeSource: hasNativeSource,
365
+ rustSource: files.some(file => /(?:^|\/)Cargo\.toml$/.test(file.path) || /\.rs$/.test(file.path)),
366
+ cSource: files.some(file => /\.(?:c|cc|cpp)$/.test(file.path)),
367
+ nodeGyp: files.some(file => /(?:^|\/)binding\.gyp$/.test(file.path)),
363
368
  dependencies: {
364
369
  core: dependencyCheck.core,
365
370
  present: dependencyCheck.present.slice(0, 100),
package/lib/convert.js CHANGED
@@ -47,12 +47,13 @@ function safeName(name) {
47
47
  */
48
48
  export async function convert(spec, options = {}) {
49
49
  const report = options.report ?? await analyze(spec, { textScan: false, signal: options.signal, registry: options.registry })
50
- if (report.package?.name === 'dsh-ops' && report.rule?.nativeBuild?.language === 'rust') {
51
- const { convertDshOps } = await import('./recipes/dsh-ops.js')
52
- return await convertDshOps(spec, {
50
+ if (report.strategy === 'S2') {
51
+ const { convertS2Native } = await import('./s2-native.js')
52
+ return await convertS2Native(spec, {
53
53
  report,
54
54
  workerPath: options.workerPath,
55
55
  workerBinary: options.workerBinary,
56
+ workerName: options.workerName,
56
57
  signal: options.signal,
57
58
  })
58
59
  }
package/lib/elf.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Minimal AArch64 ELF checks shared by the generic S2 adapter and the generic
3
+ * sidecar staging path.
4
+ *
5
+ * @module dshm-compat/elf
6
+ */
7
+
8
+ /** @param {Buffer} buffer */
9
+ export function isAarch64Elf(buffer) {
10
+ return buffer.length >= 64
11
+ && buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46
12
+ && buffer[4] === 2 && buffer[5] === 1
13
+ && buffer.readUInt16LE(18) === 183
14
+ }
15
+
16
+ /** @param {Buffer} buffer */
17
+ export function isX64Elf(buffer) {
18
+ return buffer.length >= 64
19
+ && buffer[0] === 0x7f && buffer[1] === 0x45 && buffer[2] === 0x4c && buffer[3] === 0x46
20
+ && buffer[4] === 2 && buffer[5] === 1
21
+ && buffer.readUInt16LE(18) === 62
22
+ }
23
+
24
+ /** @param {Buffer} buffer @returns {'arm64'|'x64'|undefined} */
25
+ export function elfArch(buffer) {
26
+ if (isAarch64Elf(buffer)) return 'arm64'
27
+ if (isX64Elf(buffer)) return 'x64'
28
+ return undefined
29
+ }
30
+
31
+ /** @param {Buffer} buffer */
32
+ export function elfSummary(buffer) {
33
+ if (!isAarch64Elf(buffer)) return undefined
34
+ return {
35
+ class: 64,
36
+ endian: 'little',
37
+ machine: 'AArch64',
38
+ type: buffer.readUInt16LE(16) === 3 ? 'DYN' : buffer.readUInt16LE(16) === 2 ? 'EXEC' : String(buffer.readUInt16LE(16)),
39
+ bytes: buffer.length,
40
+ }
41
+ }
package/lib/guest.js CHANGED
@@ -8,6 +8,8 @@
8
8
  *
9
9
  * @module dshm-compat/guest
10
10
  */
11
+ import { isAbsolute } from 'node:path'
12
+
11
13
  const PROBE_SCRIPT = [
12
14
  'echo DSHM_COMPAT_PROBE_BEGIN',
13
15
  'uname -srm 2>/dev/null || true',
@@ -19,7 +21,6 @@ const PROBE_SCRIPT = [
19
21
  'command -v python3 || true',
20
22
  'echo DSHM_COMPAT_PROBE_END',
21
23
  ].join('\n')
22
-
23
24
  /** Packages the provisioning tool may install. */
24
25
  export const PROVISION_PACKAGES = Object.freeze([
25
26
  'bash',
@@ -98,7 +99,7 @@ export async function runGuest(ctx, script, options = {}) {
98
99
  * @param {{ timeoutMs?: number, signal?: AbortSignal, cwd?: string }} [options]
99
100
  */
100
101
  export async function runGuestFile(ctx, file, args = [], input = '', options = {}) {
101
- if (typeof file !== 'string' || !file.startsWith('/')) {
102
+ if (typeof file !== 'string' || !isAbsolute(file)) {
102
103
  return { status: 'invalid', reason: 'worker path must be absolute and visible inside the guest' }
103
104
  }
104
105
  const subprocess = ctx.get('subprocess')
package/lib/plan.js CHANGED
@@ -18,6 +18,16 @@ function record(value) {
18
18
  function nativeBuildOf(report) {
19
19
  const rule = record(report.rule)
20
20
  if (record(rule?.nativeBuild) !== undefined) return rule.nativeBuild
21
+ const signals = record(report.signals) ?? {}
22
+ if (signals.nodeGyp === true) {
23
+ return { language: 'node-gyp', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest with build-base and python3' }
24
+ }
25
+ if (signals.rustSource === true) {
26
+ return { language: 'rust', target: 'aarch64-unknown-linux-musl' }
27
+ }
28
+ if (signals.cSource === true) {
29
+ return { language: 'c/c++', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest' }
30
+ }
21
31
  const native = Array.isArray(report.files?.native) ? report.files.native : []
22
32
  if (native.some(file => typeof file.path === 'string' && file.path.endsWith('binding.gyp'))) {
23
33
  return { language: 'node-gyp', target: 'aarch64-unknown-linux-musl', notes: 'build in the Alpine guest with build-base and python3' }