Explorar o código

fix(build): validate packaged spawn helpers

Tianyi Cui hai 2 meses
pai
achega
7b1e8978a9

+ 2 - 2
.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
-2026-07-29-persistent-bash-str-replace-editor.md: 26949e7435bcbf05132662c308f33f322920c7eb
-2026-07-29-persistent-bash-str-replace-editor.zh.md: 6d5eadbfa68a59157e8f4bc148d03144bc665cb2
+2026-07-29-persistent-bash-str-replace-editor.md: b1be9cc40e11b07b722877e666de0e0328636a05
+2026-07-29-persistent-bash-str-replace-editor.zh.md: 0bf100514e971dfa759f44dcca15a7ad6a2fdd8a

+ 1 - 1
.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md

@@ -16,7 +16,7 @@ Some deployments need a one-call Bash schema whose shell state survives across m
 
 `dsh-system-prompt` accepts `includeHarnessIdentity: false`, while `dsh-agent-spine-demo` forwards that setting and accepts `toolBash: false`. A deployment can therefore own an exact persona and replace the spine's native Bash without duplicate prompt or tool registrations. Existing defaults remain unchanged.
 
-Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper`, each packaged runtime executable ships with an architecture-matched `-spawn-helper` sibling. A pinned `node-pty` patch resolves that sibling only when present (or when `DSH_NODE_PTY_SPAWN_HELPER` explicitly selects one), preserving upstream lookup in ordinary Node runs; the executable and runtime-wheel builders fail before publication when the helper is absent, mismatched, or not executable.
+Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper`, each packaged runtime executable ships with an architecture-matched `-spawn-helper` sibling. A pinned `node-pty` patch resolves that sibling only when present, preserving upstream lookup in ordinary Node runs. The explicit `DSH_NODE_PTY_SPAWN_HELPER` override remains for a current external consumer that supplies a non-sibling helper. The executable and runtime-wheel builders inspect ELF or thin Mach-O headers and fail before publication when the helper is absent, mismatched, or not executable.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md

@@ -16,7 +16,7 @@
 
 `dsh-system-prompt` 接受 `includeHarnessIdentity: false`;`dsh-agent-spine-demo` 会转发该设置,并接受 `toolBash: false`。因此部署可以拥有精确 persona,并替换 spine 的原生 Bash,而不会重复注册提示词或工具。既有默认值不变。
 
-两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 会执行原生 `spawn-helper`,每个打包后的运行时可执行文件都会携带一个架构匹配的 `-spawn-helper` 伴随文件。固定版本的 `node-pty` 补丁只在该伴随文件存在时解析它(也可由 `DSH_NODE_PTY_SPAWN_HELPER` 显式指定),普通 Node 运行仍保留上游查找方式;若 helper 缺失、架构不匹配或不可执行,可执行文件与 runtime wheel 构建会在发布前失败。
+两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 会执行原生 `spawn-helper`,每个打包后的运行时可执行文件都会携带一个架构匹配的 `-spawn-helper` 伴随文件。固定版本的 `node-pty` 补丁只在该伴随文件存在时解析它,普通 Node 运行仍保留上游查找方式。显式的 `DSH_NODE_PTY_SPAWN_HELPER` 覆盖仍予保留,供当前提供非伴随 helper 的外部消费方使用。可执行文件与运行时 wheel 包的构建器会检查 ELF 或 thin Mach-O 文件头;若 helper 缺失、架构不匹配或不可执行,构建会在发布前失败。
 
 ## 考虑过的替代方案
 

+ 4 - 2
patches/node-pty@1.1.0.patch

@@ -2,7 +2,7 @@ diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js
 index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf988cf94c5 100644
 --- a/lib/unixTerminal.js
 +++ b/lib/unixTerminal.js
-@@ -26,10 +26,22 @@ var terminal_1 = require("./terminal");
+@@ -26,10 +26,23 @@ var terminal_1 = require("./terminal");
  var utils_1 = require("./utils");
  var native = utils_1.loadNativeModule('pty');
  var pty = native.module;
@@ -10,6 +10,7 @@ index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf9
 -helperPath = path.resolve(__dirname, helperPath);
 -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
 -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
++// A current external embedded-runtime consumer supplies a non-sibling helper.
 +var helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
 +if (helperPath) {
 +    helperPath = path.resolve(helperPath);
@@ -33,7 +34,7 @@ diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts
 index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf9771220392d17 100644
 --- a/src/unixTerminal.ts
 +++ b/src/unixTerminal.ts
-@@ -14,10 +14,20 @@ import { assign, loadNativeModule } from './utils';
+@@ -14,10 +14,21 @@ import { assign, loadNativeModule } from './utils';
 
  const native = loadNativeModule('pty');
  const pty: IUnixNative = native.module;
@@ -41,6 +42,7 @@ index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf97712
 -helperPath = path.resolve(__dirname, helperPath);
 -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
 -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
++// A current external embedded-runtime consumer supplies a non-sibling helper.
 +let helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
 +if (helperPath) {
 +  helperPath = path.resolve(helperPath);

+ 4 - 4
pnpm-lock.yaml

@@ -6,7 +6,7 @@ settings:
 
 patchedDependencies:
   '@earendil-works/pi-tui@0.80.7': 6c30c5386c0159131e1361023cddf31377f5728962524841964373312c1ed946
-  node-pty@1.1.0: 4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15
+  node-pty@1.1.0: 7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6
 
 importers:
 
@@ -656,7 +656,7 @@ importers:
     devDependencies:
       node-pty:
         specifier: 1.1.0
-        version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15)
+        version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6)
 
   packages/acp/acp:
     dependencies:
@@ -3402,7 +3402,7 @@ importers:
     dependencies:
       node-pty:
         specifier: ^1.1.0
-        version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15)
+        version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6)
       schemastery:
         specifier: ^3.18.0
         version: 3.18.0
@@ -15287,7 +15287,7 @@ snapshots:
       fetch-blob: 3.2.0
       formdata-polyfill: 4.0.10
 
-  node-pty@1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15):
+  node-pty@1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6):
     dependencies:
       node-addon-api: 7.1.1
 

+ 2 - 2
python/sdk-runtime/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
-README.md: ee3791eddf26b526316d4f3952793a03cc48841e
-README.zh.md: 59d40ee56688cb377902ff126b7fa77606c7ad8b
+README.md: 0869b4a9dce0e261b168f21c90a80faf81ea4a64
+README.zh.md: 3f2342c4c66e24b8213c78d4e7530c3360497022

+ 1 - 1
python/sdk-runtime/README.md

@@ -8,7 +8,7 @@ Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`,
 
 Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored:
 
-- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` plus its native `-spawn-helper` sibling (platform: `linux`/`macos`; arch: `x64`/`arm64`). The helper is required by `node-pty`; both files are built and validated as one runtime product. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists.
+- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` plus its native `-spawn-helper` sibling (platform: `linux`/`macos`; arch: `x64`/`arm64`). The helper is required by `node-pty`; both files are built as one runtime product, and ELF or thin Mach-O headers must match the target. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists.
 - **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions.
 
 Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding.

+ 1 - 1
python/sdk-runtime/README.zh.md

@@ -8,7 +8,7 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,
 
 两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略:
 
-- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 及其原生 `-spawn-helper` 伴随文件(platform:`linux`/`macos`;arch:`x64`/`arm64`)。`node-pty` 需要该 helper;构建与校验会把两者视作同一个运行时产物。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
+- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 及其原生 `-spawn-helper` 伴随文件(platform:`linux`/`macos`;arch:`x64`/`arm64`)。`node-pty` 需要该 helper;两者作为一个运行时产物构建,且 ELF 或 thin Mach-O 文件头必须与目标匹配。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
 - **`node`(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
 
 两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。

+ 31 - 2
python/sdk-runtime/hatch_build.py

@@ -16,6 +16,34 @@ _PLATFORMS = {
 _SPAWN_HELPER_SUFFIX = "-spawn-helper"
 
 
+def _spawn_helper_binary_target(header: bytes) -> str | None:
+    if (
+        len(header) >= 20
+        and header[:4] == b"\x7fELF"
+        and header[4] == 2
+        and header[5] == 1
+    ):
+        machine = int.from_bytes(header[18:20], "little")
+        if machine == 62:
+            return "linux-x64"
+        if machine == 183:
+            return "linux-arm64"
+    if len(header) >= 8 and header[:4] == b"\xcf\xfa\xed\xfe":
+        if int.from_bytes(header[4:8], "little") == 0x0100000C:
+            return "macos-arm64"
+    return None
+
+
+def _validate_spawn_helper(path: Path, expected_target: str) -> None:
+    with path.open("rb") as helper:
+        actual_target = _spawn_helper_binary_target(helper.read(20))
+    if actual_target != expected_target:
+        raise RuntimeError(
+            f"runtime spawn helper binary mismatch: expected {expected_target}, "
+            f"found {actual_target or 'unsupported format or architecture'} at {path}"
+        )
+
+
 def _host_platform_tag() -> str:
     machine = platform.machine().lower()
     arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
@@ -39,13 +67,13 @@ class RuntimeBuildHook(BuildHookInterface):
             )
 
         platform_tag = os.environ.get("DSH_RUNTIME_PLATFORM_TAG") or _host_platform_tag()
-        matches = [value for value in _PLATFORMS.values() if value[0] == platform_tag]
+        matches = [(key, value) for key, value in _PLATFORMS.items() if value[0] == platform_tag]
         if len(matches) != 1:
             supported = ", ".join(value[0] for value in _PLATFORMS.values())
             raise RuntimeError(
                 f"unsupported DSH_RUNTIME_PLATFORM_TAG {platform_tag!r}; expected one of {supported}"
             )
-        expected_executable = matches[0][1]
+        expected_target, (_, expected_executable) = matches[0]
         runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
         runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
         executables = [path for path in runtime_files if not path.name.endswith(_SPAWN_HELPER_SUFFIX)]
@@ -64,6 +92,7 @@ class RuntimeBuildHook(BuildHookInterface):
         for executable in [executables[0], helpers[0]]:
             if executable.stat().st_mode & stat.S_IXUSR == 0:
                 raise RuntimeError(f"runtime executable is not executable: {executable}")
+        _validate_spawn_helper(helpers[0], expected_target)
 
         build_data["pure_python"] = False
         build_data["infer_tag"] = False

+ 53 - 2
python/sdk/tests/test_release_version.py

@@ -16,6 +16,18 @@ SCRIPT = ROOT / "scripts" / "build-python-release.py"
 build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
 
 
+def helper_header(target: str) -> bytes:
+    header = bytearray(20)
+    if target.startswith("linux-"):
+        header[:6] = b"\x7fELF\x02\x01"
+        machine = 62 if target == "linux-x64" else 183
+        header[18:20] = machine.to_bytes(2, "little")
+    else:
+        header[:4] = b"\xcf\xfa\xed\xfe"
+        header[4:8] = (0x0100000C).to_bytes(4, "little")
+    return bytes(header)
+
+
 def test_repository_version_matches_root_package_json() -> None:
     expected = json.loads((ROOT / "package.json").read_text())["version"]
 
@@ -45,7 +57,7 @@ def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> Non
     executable.write_bytes(b"runtime")
     executable.chmod(0o755)
     spawn_helper = Path(f"{executable}-spawn-helper")
-    spawn_helper.write_bytes(b"helper")
+    spawn_helper.write_bytes(helper_header("macos-arm64"))
     spawn_helper.chmod(0o751)
     destination = tmp_path / "staging"
 
@@ -59,7 +71,7 @@ def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> Non
     runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
     assert (runtime_dir / executable.name).read_bytes() == b"runtime"
     copied_helper = runtime_dir / spawn_helper.name
-    assert copied_helper.read_bytes() == b"helper"
+    assert copied_helper.read_bytes() == helper_header("macos-arm64")
     assert copied_helper.stat().st_mode & stat.S_IXUSR
 
 
@@ -75,3 +87,42 @@ def test_stage_runtime_rejects_missing_spawn_helper(tmp_path: Path) -> None:
             executable,
             executable.name,
         )
+
+
+@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64", "macos-arm64"])
+def test_spawn_helper_binary_target(target: str) -> None:
+    assert build_python_release.spawn_helper_binary_target(helper_header(target)) == target
+
+
+def test_stage_runtime_rejects_mismatched_spawn_helper(tmp_path: Path) -> None:
+    executable = tmp_path / "dsh-jsonrpc-agent-pkg-linux-x64"
+    executable.write_bytes(b"runtime")
+    executable.chmod(0o755)
+    spawn_helper = Path(f"{executable}-spawn-helper")
+    spawn_helper.write_bytes(helper_header("linux-arm64"))
+    spawn_helper.chmod(0o755)
+
+    with pytest.raises(ValueError, match="expected linux-x64, found linux-arm64"):
+        build_python_release.stage_runtime(
+            tmp_path / "staging",
+            "1.2.3",
+            executable,
+            executable.name,
+        )
+
+
+def test_stage_runtime_rejects_non_binary_spawn_helper(tmp_path: Path) -> None:
+    executable = tmp_path / "dsh-jsonrpc-agent-pkg-macos-arm64"
+    executable.write_bytes(b"runtime")
+    executable.chmod(0o755)
+    spawn_helper = Path(f"{executable}-spawn-helper")
+    spawn_helper.write_bytes(b"helper")
+    spawn_helper.chmod(0o755)
+
+    with pytest.raises(ValueError, match="unsupported format or architecture"):
+        build_python_release.stage_runtime(
+            tmp_path / "staging",
+            "1.2.3",
+            executable,
+            executable.name,
+        )

+ 30 - 2
scripts/build-exe-for-python-sdk.ts

@@ -7,7 +7,7 @@
  */
 
 import { spawn } from 'node:child_process'
-import { existsSync, mkdirSync, statSync } from 'node:fs'
+import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
 import { chmod, copyFile, readFile, rm, writeFile } from 'node:fs/promises'
 import { basename, join, resolve, sep } from 'node:path'
 import { parseArgs } from 'node:util'
@@ -58,6 +58,24 @@ interface RuntimeProduct {
   spawnHelper: string
 }
 
+function spawnHelperBinaryTarget(path: string): string | undefined {
+  const header = readFileSync(path).subarray(0, 20)
+  if (header.length >= 20
+    && header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
+    && header[4] === 2
+    && header[5] === 1) {
+    const machine = header.readUInt16LE(18)
+    if (machine === 62) return 'linux-x64'
+    if (machine === 183) return 'linux-arm64'
+  }
+  if (header.length >= 8 && header.readUInt32LE(0) === 0xfeedfacf) {
+    const cpuType = header.readUInt32LE(4)
+    if (cpuType === 0x01000007) return 'macos-x64'
+    if (cpuType === 0x0100000c) return 'macos-arm64'
+  }
+  return undefined
+}
+
 function isPlatform(value: string): value is Platform {
   return (PLATFORMS as readonly string[]).includes(value)
 }
@@ -347,7 +365,17 @@ class SingleExeBuild {
         + `checked ${candidates.join(', ')}. Build each runtime on its target platform and architecture.`,
       )
     }
-    if (statSync(helper).mode & 0o111) return helper
+    if (statSync(helper).mode & 0o111) {
+      const expected = `${target.platform}-${target.arch}`
+      const actual = spawnHelperBinaryTarget(helper)
+      if (actual !== expected) {
+        throw new Error(
+          `build-exe-for-python-sdk: node-pty spawn-helper binary mismatch: expected ${expected}, `
+          + `found ${actual ?? 'unsupported format or architecture'} at ${helper}`,
+        )
+      }
+      return helper
+    }
     throw new Error(`build-exe-for-python-sdk: node-pty spawn-helper is not executable: ${helper}`)
   }
 

+ 37 - 0
scripts/build-python-release.py

@@ -23,6 +23,35 @@ PLATFORMS = {
     "macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
 }
 SPAWN_HELPER_SUFFIX = "-spawn-helper"
+EXECUTABLE_TARGETS = {value[1]: key for key, value in PLATFORMS.items()}
+
+
+def spawn_helper_binary_target(header: bytes) -> str | None:
+    if (
+        len(header) >= 20
+        and header[:4] == b"\x7fELF"
+        and header[4] == 2
+        and header[5] == 1
+    ):
+        machine = int.from_bytes(header[18:20], "little")
+        if machine == 62:
+            return "linux-x64"
+        if machine == 183:
+            return "linux-arm64"
+    if len(header) >= 8 and header[:4] == b"\xcf\xfa\xed\xfe":
+        if int.from_bytes(header[4:8], "little") == 0x0100000C:
+            return "macos-arm64"
+    return None
+
+
+def validate_spawn_helper(path: Path, expected_target: str) -> None:
+    with path.open("rb") as helper:
+        actual_target = spawn_helper_binary_target(helper.read(20))
+    if actual_target != expected_target:
+        raise ValueError(
+            f"runtime spawn helper binary mismatch: expected {expected_target}, "
+            f"found {actual_target or 'unsupported format or architecture'} at {path}"
+        )
 
 
 def main() -> None:
@@ -142,6 +171,7 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
         raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}")
     if spawn_helper.stat().st_mode & stat.S_IXUSR == 0:
         raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}")
+    validate_spawn_helper(spawn_helper, EXECUTABLE_TARGETS[executable_name])
     copy_package(ROOT / "python" / "sdk-runtime", destination)
     rewrite_version(destination / "pyproject.toml", version)
     runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
@@ -186,6 +216,13 @@ def verify_wheel(
                 mode = archive.getinfo(executable).external_attr >> 16
                 if mode & stat.S_IXUSR == 0:
                     raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {executable}")
+            actual_target = spawn_helper_binary_target(archive.read(helpers[0])[:20])
+            expected_target = EXECUTABLE_TARGETS[platform[1]]
+            if actual_target != expected_target:
+                raise RuntimeError(
+                    f"{wheel} spawn helper binary mismatch: expected {expected_target}, "
+                    f"found {actual_target or 'unsupported format or architecture'}"
+                )
         elif runtime_files:
             raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
         if package == "sdk":