From 7b1e8978a989cd4406e5d8fa123c82590c75de2e Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:49:21 +0800 Subject: [PATCH] fix(build): validate packaged spawn helpers --- ...rsistent-bash-str-replace-editor.i18n.yaml | 4 +- ...7-29-persistent-bash-str-replace-editor.md | 2 +- ...9-persistent-bash-str-replace-editor.zh.md | 2 +- patches/node-pty@1.1.0.patch | 6 +- pnpm-lock.yaml | 8 +-- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 2 +- python/sdk-runtime/README.zh.md | 2 +- python/sdk-runtime/hatch_build.py | 33 ++++++++++- python/sdk/tests/test_release_version.py | 55 ++++++++++++++++++- scripts/build-exe-for-python-sdk.ts | 32 ++++++++++- scripts/build-python-release.py | 37 +++++++++++++ 12 files changed, 167 insertions(+), 20 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml index df7df7c84c..cb1e7631fd 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.i18n.yaml +++ b/.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 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md index 26949e7435..b1be9cc40e 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md +++ b/.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 diff --git a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md b/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md index 6d5eadbfa6..0bf100514e 100644 --- a/.agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.zh.md +++ b/.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 缺失、架构不匹配或不可执行,构建会在发布前失败。 ## 考虑过的替代方案 diff --git a/patches/node-pty@1.1.0.patch b/patches/node-pty@1.1.0.patch index f0de7b9054..56892a3d58 100644 --- a/patches/node-pty@1.1.0.patch +++ b/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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73092a292c..82af864f54 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index 4b104ec211..c25eecf5de 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/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 diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index ee3791eddf..0869b4a9dc 100644 --- a/python/sdk-runtime/README.md +++ b/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--` 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--` 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. diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index 59d40ee566..3f2342c4c6 100644 --- a/python/sdk-runtime/README.zh.md +++ b/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--` 及其原生 `-spawn-helper` 伴随文件(platform:`linux`/`macos`;arch:`x64`/`arm64`)。`node-pty` 需要该 helper;构建与校验会把两者视作同一个运行时产物。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。 +- **exe(生产)**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg--` 及其原生 `-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/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 diff --git a/python/sdk-runtime/hatch_build.py b/python/sdk-runtime/hatch_build.py index 108e77cf2c..f2df169cfc 100644 --- a/python/sdk-runtime/hatch_build.py +++ b/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 diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index 7b7c8254b1..698676a636 100644 --- a/python/sdk/tests/test_release_version.py +++ b/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, + ) diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 38400009ac..24cd6c0383 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/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}`) } diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index 915968b30d..3011120da1 100644 --- a/scripts/build-python-release.py +++ b/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":