Przeglądaj źródła

feat(python-runtime): package the Windows x64 dsh executable

Add node24-win-x64 as the only supported Windows runtime target and publish it as a py3-none-win_amd64 wheel containing the conventional dsh and ripgrep .exe payload names. Keep Windows ARM64 rejected explicitly so Python cannot claim a carrier that CI and release automation do not build.

Teach the pkg builder to require a native x64 Windows host, validate both node-pty ConPTY addons, copy @vscode's win32 ripgrep executable, and recognize pkg's .exe output. Extend runtime resolution, wheel staging, payload validation, and the preset closure check so the Windows-specific PowerShell plugins and sidecars fail loud when omitted.

The sidecar resolver now maps a packaged main.exe to main-rg.exe; focused TypeScript and Python tests cover that name, the win_amd64 manifest, x64-only host selection, complete wheel payload, ConPTY inventory, and platform-conditioned plugin closure.
Tianyi Cui 2 tygodni temu
rodzic
commit
ca0b21661e

+ 5 - 2
packages/fs/tool-fs-search/src/search-core.ts

@@ -20,7 +20,7 @@
  */
 
 import { existsSync } from 'node:fs'
-import { isAbsolute, relative, sep } from 'node:path'
+import { isAbsolute, join, parse, relative, sep } from 'node:path'
 import type { Context } from '@deepseek-ai/cordis'
 import { HarnessError } from '@deepseek-ai/dsh-llm'
 import { ItemRetainer, TextRetainer } from '@deepseek-ai/dsh-output-retention'
@@ -170,7 +170,10 @@ let rgPathPromise: Promise<string> | undefined
  */
 export function resolveRgPath(): Promise<string> {
   rgPathPromise ??= Promise.resolve().then(async () => {
-    const executableSidecar = `${process.execPath}-rg`
+    const executable = parse(process.execPath)
+    const executableSidecar = process.platform === 'win32'
+      ? join(executable.dir, `${executable.name}-rg.exe`)
+      : `${process.execPath}-rg`
     if ('pkg' in process && existsSync(executableSidecar)) return executableSidecar
     return (await import('@vscode/ripgrep')).rgPath
   })

+ 27 - 2
packages/fs/tool-fs-search/tests/rg-sidecar.spec.ts

@@ -1,9 +1,12 @@
+import { join, parse } from 'node:path'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 
 const { dependencyRgPath, existsSync } = vi.hoisted(() => ({
   dependencyRgPath: '/node_modules/@vscode/ripgrep/bin/rg',
   existsSync: vi.fn(),
 }))
+const originalPlatform = process.platform
+const originalExecPath = process.execPath
 
 vi.mock('node:fs', async (importOriginal) => {
   const actual = await importOriginal<typeof import('node:fs')>()
@@ -16,17 +19,35 @@ beforeEach(() => {
   vi.resetModules()
   existsSync.mockReset()
   Reflect.deleteProperty(process, 'pkg')
+  Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: originalPlatform })
+  process.execPath = originalExecPath
 })
 
 afterEach(() => {
   Reflect.deleteProperty(process, 'pkg')
+  Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: originalPlatform })
+  process.execPath = originalExecPath
 })
 
 describe('ripgrep resolution', () => {
   it('uses the native sidecar beside the current executable', async () => {
     Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: 'linux' })
+    process.execPath = '/runtime/dsh'
     existsSync.mockReturnValue(true)
-    const sidecar = `${process.execPath}-rg`
+    const sidecar = '/runtime/dsh-rg'
+    const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
+
+    await expect(resolveRgPath()).resolves.toBe(sidecar)
+    expect(existsSync).toHaveBeenCalledWith(sidecar)
+  })
+
+  it('uses a conventional executable name for the Windows ripgrep sidecar', async () => {
+    Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: 'win32' })
+    process.execPath = 'C:\\runtime\\deepseek-harness-sdk-runtime-win-x64.exe'
+    existsSync.mockReturnValue(true)
+    const sidecar = 'C:\\runtime\\deepseek-harness-sdk-runtime-win-x64-rg.exe'
     const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
 
     await expect(resolveRgPath()).resolves.toBe(sidecar)
@@ -47,6 +68,10 @@ describe('ripgrep resolution', () => {
     const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
 
     await expect(resolveRgPath()).resolves.toBe(dependencyRgPath)
-    expect(existsSync).toHaveBeenCalledWith(`${process.execPath}-rg`)
+    const executable = parse(process.execPath)
+    const sidecar = process.platform === 'win32'
+      ? join(executable.dir, `${executable.name}-rg.exe`)
+      : `${process.execPath}-rg`
+    expect(existsSync).toHaveBeenCalledWith(sidecar)
   })
 })

+ 6 - 0
pnpm-lock.yaml

@@ -9972,6 +9972,12 @@ importers:
       '@deepseek-ai/dsh-tool-jobs':
         specifier: workspace:^
         version: link:../../packages/jobs/tool-jobs
+      '@deepseek-ai/dsh-tool-pwsh':
+        specifier: workspace:^
+        version: link:../../packages/shell/tool-pwsh
+      '@deepseek-ai/dsh-tool-pwsh-persistent':
+        specifier: workspace:^
+        version: link:../../packages/shell/tool-pwsh-persistent
       '@deepseek-ai/dsh-tool-ralph':
         specifier: workspace:^
         version: link:../../packages/workflow/tool-ralph

+ 16 - 3
python/sdk-runtime/hatch_build.py

@@ -39,7 +39,15 @@ 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
     system = platform.system().lower()
-    key = f"macos-{arch}" if system == "darwin" else f"linux-{arch}" if system == "linux" else system
+    key = (
+        f"macos-{arch}"
+        if system == "darwin"
+        else f"linux-{arch}"
+        if system == "linux"
+        else f"win-{arch}"
+        if system == "windows"
+        else system
+    )
     try:
         return _PLATFORMS[key][0]
     except KeyError as exc:
@@ -69,16 +77,21 @@ class RuntimeBuildHook(BuildHookInterface):
         runtime_files = sorted(
             runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else []
         )
-        expected_files = [expected_executable, f"{expected_executable}-rg"]
+        expected_files = (
+            [expected_executable, f"{expected_executable.removesuffix('.exe')}-rg.exe"]
+            if expected_executable.endswith(".exe")
+            else [expected_executable, f"{expected_executable}-rg"]
+        )
         if "-macos-" in expected_executable:
             expected_files.append(f"{expected_executable}-spawn-helper")
+        expected_files.sort()
         found_files = [path.name for path in runtime_files]
         if found_files != expected_files:
             raise RuntimeError(
                 f"runtime wheel {platform_tag} payload must be {expected_files}; found {found_files}"
             )
         for executable in runtime_files:
-            if executable.stat().st_mode & stat.S_IXUSR == 0:
+            if platform_tag != "win_amd64" and executable.stat().st_mode & stat.S_IXUSR == 0:
                 raise RuntimeError(f"runtime executable is not executable: {executable}")
         build_data["pure_python"] = False
         build_data["infer_tag"] = False

+ 2 - 0
python/sdk-runtime/package.json

@@ -59,6 +59,7 @@
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
     "@deepseek-ai/dsh-persona": "workspace:^",
     "@deepseek-ai/dsh-pwsh-local": "workspace:^",
+    "@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
     "@deepseek-ai/dsh-terminal": "workspace:^",
     "@deepseek-ai/dsh-terminal-bash": "workspace:^",
     "@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
@@ -103,6 +104,7 @@
     "@deepseek-ai/dsh-tool-fs": "workspace:^",
     "@deepseek-ai/dsh-tool-fs-search": "workspace:^",
     "@deepseek-ai/dsh-tool-goal": "workspace:^",
+    "@deepseek-ai/dsh-tool-pwsh": "workspace:^",
     "@deepseek-ai/dsh-tool-ralph": "workspace:^",
     "@deepseek-ai/dsh-tool-skill": "workspace:^",
     "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",

+ 4 - 0
python/sdk-runtime/platforms.json

@@ -10,5 +10,9 @@
   "macos-arm64": {
     "tag": "macosx_14_0_arm64",
     "executable": "deepseek-harness-sdk-runtime-macos-arm64"
+  },
+  "win-x64": {
+    "tag": "win_amd64",
+    "executable": "deepseek-harness-sdk-runtime-win-x64.exe"
   }
 }

+ 14 - 8
python/sdk-runtime/src/deepseek_harness_runtime/__init__.py

@@ -4,9 +4,10 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
 ``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
 
 - **exe (production)**: single-file Node executables named
-  ``deepseek-harness-sdk-runtime-<platform>-<arch>`` (platform in {linux, macos}, arch in
-  {x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling
-  ``-spawn-helper``. The target machine needs no Node installation.
+  ``deepseek-harness-sdk-runtime-<platform>-<arch>`` for Linux/macOS and an
+  ``.exe`` counterpart for Windows. Each has a sibling ripgrep executable;
+  macOS also uses a sibling ``-spawn-helper``. The target machine needs no
+  Node installation.
 - **node (dev-only)**: the full deploy closure under ``runtime/node/``
   (``package.json`` + ``node_modules/``), executed as ``node
   runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a
@@ -30,7 +31,7 @@ PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
 
 RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
 
-_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos"}
+_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos", "win32": "win"}
 _ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
 
 _EXE_ACQUISITION_HINT = (
@@ -62,13 +63,18 @@ def bundled_runtime_path() -> Path:
     touching callers).
     """
     tag = _current_platform_tag()
-    path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}"
+    extension = ".exe" if tag.startswith("win-") else ""
+    path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}{extension}"
     if not path.is_file():
         raise FileNotFoundError(
             f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
             + _EXE_ACQUISITION_HINT
         )
-    ripgrep = Path(f"{path}-rg")
+    ripgrep = (
+        path.with_name(f"{path.stem}-rg.exe")
+        if tag.startswith("win-")
+        else Path(f"{path}-rg")
+    )
     if not ripgrep.is_file():
         raise FileNotFoundError(
             f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
@@ -110,11 +116,11 @@ def resolve_bundled_launch_args(mode: str | None = None) -> tuple[str, ...]:
 def _current_platform_tag() -> str:
     plat = _PLATFORM_TAGS.get(sys.platform)
     arch = _ARCH_TAGS.get(platform.machine().lower())
-    if plat is None or arch is None:
+    if plat is None or arch is None or (plat == "win" and arch != "x64"):
         raise FileNotFoundError(
             "no bundled DeepSeek Harness SDK runtime exists for this platform "
             f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
-            "linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT
+            "Linux x64/arm64, macOS arm64, and Windows x64. " + _EXE_ACQUISITION_HINT
         )
     return f"{plat}-{arch}"
 

+ 14 - 5
python/sdk/src/deepseek_harness/client.py

@@ -93,11 +93,14 @@ class HarnessClient:
         self._start_stderr_thread()
 
     def close(self) -> None:
+        """Close the runtime after a bounded opportunity to flush durable state."""
         proc = self._proc
         if proc is None:
             return
+        shutdown_completed = False
         try:
             self.request("shutdown", None, response_model=_ShutdownResponse, timeout_seconds=self.config.shutdown_timeout_seconds)
+            shutdown_completed = True
         except Exception as exc:
             self._stderr_lines.append(f"shutdown request failed: {exc}")
         if proc.stdin:
@@ -105,16 +108,22 @@ class HarnessClient:
                 proc.stdin.close()
             except Exception as exc:
                 self._stderr_lines.append(f"stdin close failed: {exc}")
+        if shutdown_completed:
+            try:
+                proc.wait(timeout=self.config.shutdown_timeout_seconds)
+            except subprocess.TimeoutExpired:
+                pass
         if proc.poll() is None:
             try:
                 proc.terminate()
             except ProcessLookupError:
                 pass
-        try:
-            proc.wait(timeout=self.config.shutdown_timeout_seconds)
-        except subprocess.TimeoutExpired:
-            proc.kill()
-            proc.wait()
+        if proc.poll() is None:
+            try:
+                proc.wait(timeout=self.config.shutdown_timeout_seconds)
+            except subprocess.TimeoutExpired:
+                proc.kill()
+                proc.wait()
         self._proc = None
         self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime closed"))
         if self._reader_thread and self._reader_thread.is_alive():

+ 37 - 0
python/sdk/tests/test_client.py

@@ -783,6 +783,43 @@ for line in sys.stdin:
     assert client._proc is None
 
 
+def test_client_close_allows_eof_quiescence_after_shutdown_response(tmp_path: Path) -> None:
+    script = tmp_path / "fake_runtime.py"
+    marker = tmp_path / "quiesced.txt"
+    script.write_text(
+        """
+import json
+import os
+from pathlib import Path
+import sys
+import time
+
+for line in sys.stdin:
+    msg = json.loads(line)
+    if msg.get("method") == "initialize":
+        print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
+    elif msg.get("method") == "shutdown":
+        print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
+
+time.sleep(0.05)
+Path(os.environ["QUIESCED_MARKER"]).write_text("quiesced")
+""".strip()
+    )
+
+    client = HarnessClient(
+        HarnessConfig(
+            _launch_args=(sys.executable, str(script)),
+            env={"QUIESCED_MARKER": str(marker)},
+            shutdown_timeout_seconds=1,
+        )
+    )
+    client.start()
+    client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
+    client.close()
+
+    assert marker.read_text() == "quiesced"
+
+
 def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
     script = tmp_path / "rejecting_runtime.py"
     script.write_text(

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

@@ -62,6 +62,14 @@ def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None:
     assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64"
 
 
+def test_windows_wheel_tag_and_payload_are_x64_only() -> None:
+    assert build_python_release.PLATFORMS["win-x64"] == (
+        "win_amd64",
+        "deepseek-harness-sdk-runtime-win-x64.exe",
+    )
+    assert not any(name.startswith("win-") and name != "win-x64" for name in build_python_release.PLATFORMS)
+
+
 def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None:
     manifest = tmp_path / "platforms.json"
     manifest.write_text('{"macos-arm64":{"tag":"macosx_14_0_arm64"}}\n')
@@ -85,7 +93,10 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path:
     assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
 
 
-@pytest.mark.parametrize(("target", "with_helper"), [("linux-x64", False), ("macos-arm64", True)])
+@pytest.mark.parametrize(
+    ("target", "with_helper"),
+    [("linux-x64", False), ("macos-arm64", True), ("win-x64.exe", False)],
+)
 def test_stage_runtime_copies_platform_payload(
     tmp_path: Path, target: str, with_helper: bool
 ) -> None:
@@ -93,7 +104,11 @@ def test_stage_runtime_copies_platform_payload(
     executable.write_bytes(b"runtime")
     executable.chmod(0o755)
     expected = {executable.name: b"runtime"}
-    ripgrep = Path(f"{executable}-rg")
+    ripgrep = (
+        executable.with_name(f"{executable.stem}-rg.exe")
+        if executable.suffix == ".exe"
+        else Path(f"{executable}-rg")
+    )
     ripgrep.write_bytes(b"ripgrep")
     ripgrep.chmod(0o755)
     expected[ripgrep.name] = b"ripgrep"

+ 24 - 0
python/sdk/tests/test_runtime_resolution.py

@@ -55,6 +55,30 @@ def test_runtime_requires_spawn_helper_only_on_macos(
     assert runtime.bundled_runtime_path() == linux
 
 
+def test_windows_runtime_uses_exe_payload_and_exe_sidecar(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    runtime_dir = tmp_path / "runtime"
+    runtime_dir.mkdir()
+    executable = runtime_dir / "deepseek-harness-sdk-runtime-win-x64.exe"
+    executable.touch()
+    (runtime_dir / "deepseek-harness-sdk-runtime-win-x64-rg.exe").touch()
+    monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
+    monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "win-x64")
+
+    assert runtime.bundled_runtime_path() == executable
+
+
+def test_current_platform_supports_windows_x64_only(monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setattr(runtime.sys, "platform", "win32")
+    monkeypatch.setattr(runtime.platform, "machine", lambda: "AMD64")
+    assert runtime._current_platform_tag() == "win-x64"
+
+    monkeypatch.setattr(runtime.platform, "machine", lambda: "ARM64")
+    with pytest.raises(FileNotFoundError, match="Windows x64"):
+        runtime._current_platform_tag()
+
+
 def test_runtime_requires_ripgrep_sidecar(
     tmp_path: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:

+ 19 - 1
scripts/build-exe-for-python-sdk-native-pty.spec.ts

@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { dirname, join } from 'node:path'
 import { afterEach, describe, expect, it } from 'vitest'
-import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
+import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
 
 const roots: string[] = []
 
@@ -35,6 +35,24 @@ describe('resolveLinuxNodePtyAddon', () => {
   })
 })
 
+describe('resolveWindowsNodePtyAddons', () => {
+  it('requires both ConPTY addons from the x64 prebuild', () => {
+    const root = temporaryPackage()
+    const conpty = createAddon(root, 'prebuilds', 'win32-x64', 'conpty.node')
+    const consoleList = createAddon(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')
+
+    expect(resolveWindowsNodePtyAddons(root, 'x64')).toEqual([conpty, consoleList])
+  })
+
+  it('names every missing Windows addon', () => {
+    const root = temporaryPackage()
+
+    expect(() => resolveWindowsNodePtyAddons(root, 'x64')).toThrow(
+      `Windows node-pty addons are missing: ${join(root, 'prebuilds', 'win32-x64', 'conpty.node')}, ${join(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')}`,
+    )
+  })
+})
+
 function temporaryPackage(): string {
   const root = mkdtempSync(join(tmpdir(), 'dsh-node-pty-addon-'))
   roots.push(root)

+ 22 - 0
scripts/build-exe-for-python-sdk-native-pty.ts

@@ -21,3 +21,25 @@ export function resolveLinuxNodePtyAddon(
     `build-exe-for-python-sdk: node-pty addon is absent from both ${built} and ${prebuilt}.`,
   )
 }
+
+/**
+ * Require both node-pty addons used by the Windows ConPTY backend.
+ * @param packageDirectory - staged node-pty package directory.
+ * @param arch - Windows target architecture.
+ * @returns the existing addon paths in load order.
+ */
+export function resolveWindowsNodePtyAddons(
+  packageDirectory: string,
+  arch: 'x64',
+): string[] {
+  const directory = join(packageDirectory, 'prebuilds', `win32-${arch}`)
+  const addons = [
+    join(directory, 'conpty.node'),
+    join(directory, 'conpty_console_list.node'),
+  ]
+  const missing = addons.filter(path => !existsSync(path))
+  if (missing.length > 0) {
+    throw new Error(`build-exe-for-python-sdk: Windows node-pty addons are missing: ${missing.join(', ')}.`)
+  }
+  return addons
+}

+ 81 - 0
scripts/build-exe-for-python-sdk.spec.ts

@@ -0,0 +1,81 @@
+import { spawnSync } from 'node:child_process'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join, resolve } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+
+const root = resolve(import.meta.dirname, '..')
+const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
+const temporaryDirectories: string[] = []
+
+afterEach(() => {
+  for (const directory of temporaryDirectories.splice(0)) {
+    rmSync(directory, { recursive: true, force: true })
+  }
+})
+
+function run(env: NodeJS.ProcessEnv, ...args: string[]) {
+  return spawnSync(process.execPath, ['--import', 'tsx/esm', script, ...args], {
+    cwd: root,
+    encoding: 'utf8',
+    env: isolatedPnpmEnvironment(env),
+  })
+}
+
+describe('Python runtime executable builder CLI', () => {
+  it('runs pnpm through its JavaScript entrypoint without a command shell', () => {
+    const result = run(
+      { npm_execpath: 'C:\\tools\\pnpm.cjs' },
+      '--skip-build',
+      '--dry-run',
+      '--targets=node24-macos-arm64',
+    )
+
+    expect(result.status).toBe(0)
+    expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
+    expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
+    expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs dlx @yao-pkg/pkg@6.21.0`)
+    expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
+  })
+
+  it('resolves the pnpm package behind a Windows command shim', () => {
+    const setup = mkdtempSync(join(tmpdir(), 'dsh-pnpm-home-'))
+    temporaryDirectories.push(setup)
+    const home = join(setup, 'node_modules', '.bin')
+    const entrypoint = join(setup, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
+    mkdirSync(home, { recursive: true })
+    mkdirSync(dirname(entrypoint), { recursive: true })
+    writeFileSync(entrypoint, '')
+
+    const result = run(
+      { npm_execpath: 'C:\\tools\\pnpm.cmd', PNPM_HOME: home },
+      '--skip-build',
+      '--dry-run',
+      '--targets=node24-macos-arm64',
+    )
+
+    expect(result.status).toBe(0)
+    expect(result.stdout).toContain(`${process.execPath} ${entrypoint} run verify-runtime-closure`)
+    expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
+  })
+
+  it('rejects a Windows arm64 product before any build step', () => {
+    const result = run(
+      { npm_execpath: 'C:\\tools\\pnpm.cjs' },
+      '--skip-build',
+      '--dry-run',
+      '--targets=node24-win-arm64',
+    )
+
+    expect(result.status).not.toBe(0)
+    expect(result.stderr).toContain('Windows supports x64 only')
+    expect(result.stdout).toBe('')
+  })
+})
+
+function isolatedPnpmEnvironment(overrides: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
+  const environment = Object.fromEntries(
+    Object.entries(process.env).filter(([key]) => !['npm_execpath', 'pnpm_home'].includes(key.toLowerCase())),
+  )
+  return { ...environment, ...overrides }
+}

+ 72 - 20
scripts/build-exe-for-python-sdk.ts

@@ -9,9 +9,9 @@
 import { spawn } from 'node:child_process'
 import { existsSync, statSync } from 'node:fs'
 import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
-import { basename, dirname, join, resolve, sep } from 'node:path'
+import { basename, dirname, extname, join, resolve, sep } from 'node:path'
 import { parseArgs } from 'node:util'
-import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
+import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
 
 const root = resolve(import.meta.dirname, '..')
 
@@ -63,7 +63,7 @@ const ASSET_GLOBS = [
   'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
 ]
 
-const PLATFORMS = ['linux', 'macos'] as const
+const PLATFORMS = ['linux', 'macos', 'win'] as const
 const ARCHES = ['x64', 'arm64'] as const
 type Platform = (typeof PLATFORMS)[number]
 type Arch = (typeof ARCHES)[number]
@@ -83,10 +83,7 @@ class Target {
   private constructor(
     /** pkg Node range (`node<major>`). */
     readonly nodeRange: string,
-    /**
-     * pkg platform tag. Windows is a documented non-goal
-     * (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
-     */
+    /** pkg platform tag. */
     readonly platform: Platform,
     /** pkg CPU tag. */
     readonly arch: Arch,
@@ -117,6 +114,9 @@ class Target {
     if (!isArch(arch)) {
       throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: arch must be one of ${ARCHES.join(', ')}, got ${JSON.stringify(arch)}.`)
     }
+    if (platform === 'win' && arch !== 'x64') {
+      throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: Windows supports x64 only.`)
+    }
     return new Target(nodeRange, platform, arch)
   }
 
@@ -125,7 +125,13 @@ class Target {
    * @returns the host target; throws on an unsupported host platform or arch.
    */
   static host(): Target {
-    const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'linux' ? 'linux' : undefined
+    const platform = process.platform === 'darwin'
+      ? 'macos'
+      : process.platform === 'linux'
+        ? 'linux'
+        : process.platform === 'win32'
+          ? 'win'
+          : undefined
     if (platform === undefined) {
       throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
     }
@@ -133,6 +139,9 @@ class Target {
     if (arch === undefined) {
       throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
     }
+    if (platform === 'win' && arch !== 'x64') {
+      throw new Error('build-exe-for-python-sdk: Windows supports x64 only; use an x64 Node process.')
+    }
     return new Target(DEFAULT_NODE_RANGE, platform, arch)
   }
 }
@@ -200,7 +209,7 @@ class BuildCli {
     return [
       'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
       '',
-      '  --targets=<t1,t2,...>  pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64.',
+      '  --targets=<t1,t2,...>  pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64.',
       '                         Default: the host platform only (on node24).',
       '  --skip-build           skip `pnpm run build` (lib/ artifacts must already exist).',
       '  --dry-run              print every command and config patch without executing.',
@@ -212,8 +221,27 @@ class BuildCli {
   }
 }
 
-function pnpmBin(): string {
-  return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
+function pnpmInvocation(args: string[]): [command: string, args: string[]] {
+  const entrypoint = process.env.npm_execpath?.trim()
+  if (entrypoint !== undefined && entrypoint !== '') {
+    const extension = extname(entrypoint).toLowerCase()
+    if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
+      return [process.execPath, [entrypoint, ...args]]
+    }
+    if (extension !== '.cmd') return [entrypoint, args]
+  }
+  const home = process.env.PNPM_HOME?.trim()
+  if (home !== undefined && home !== '') {
+    const packageBin = resolve(home, '..', 'pnpm', 'bin')
+    for (const filename of ['pnpm.mjs', 'pnpm.cjs']) {
+      const candidate = resolve(packageBin, filename)
+      if (existsSync(candidate)) return [process.execPath, [candidate, ...args]]
+    }
+  }
+  if (process.platform === 'win32') {
+    throw new Error('build-exe-for-python-sdk: pnpm must expose a JavaScript entrypoint through npm_execpath or PNPM_HOME on Windows.')
+  }
+  return ['pnpm', args]
 }
 
 /**
@@ -241,7 +269,7 @@ class SingleExeBuild {
 
   /** Verify the closure before compiling or packaging. */
   async verifyClosure(): Promise<void> {
-    await this.run('runtime dependency closure', pnpmBin(), ['run', 'verify-runtime-closure'])
+    await this.runPnpm('runtime dependency closure', ['run', 'verify-runtime-closure'])
   }
 
   /** Build all package artifacts unless `--skip-build` was passed. */
@@ -250,7 +278,7 @@ class SingleExeBuild {
       console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
       return
     }
-    await this.run('build', pnpmBin(), ['run', 'build'])
+    await this.runPnpm('build', ['run', 'build'])
   }
 
   /** Clear and deploy the runtime closure into the node carrier. */
@@ -260,7 +288,7 @@ class SingleExeBuild {
     }
     if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
     else await rm(this.staging, { recursive: true, force: true })
-    await this.run('deploy', pnpmBin(), [
+    await this.runPnpm('deploy', [
       '--filter',
       DEPLOY_ROOT_PACKAGE,
       'deploy',
@@ -393,10 +421,11 @@ class SingleExeBuild {
    * @returns the executable and ripgrep sidecar paths, plus the macOS spawn helper path when required.
    */
   async pack(target: Target): Promise<string[]> {
-    const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
+    const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
+    const product = target.platform === 'win' ? `${productBase}.exe` : productBase
     await this.prepareNativePty(target)
     if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
-    await this.run(`pkg ${target.spec}`, pnpmBin(), [
+    await this.runPnpm(`pkg ${target.spec}`, [
       'dlx',
       PKG_SPEC,
       this.staging,
@@ -424,16 +453,19 @@ class SingleExeBuild {
 
   /** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
   private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
-    const platform = target.platform === 'macos' ? 'darwin' : target.platform
+    const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
+    const executable = target.platform === 'win' ? 'rg.exe' : 'rg'
     const source = join(
       this.staging,
       'node_modules',
       '@vscode',
       `ripgrep-${platform}-${target.arch}`,
       'bin',
-      'rg',
+      executable,
     )
-    const destination = `${product}-rg`
+    const destination = target.platform === 'win'
+      ? `${product.slice(0, -'.exe'.length)}-rg.exe`
+      : `${product}-rg`
     if (this.cli.dryRun) {
       console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
       return destination
@@ -455,7 +487,6 @@ class SingleExeBuild {
     const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build')
     if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
     else await rm(stagedBuild, { recursive: true, force: true })
-    if (target.platform !== 'linux') return
     const packageDirectory = join(
       root,
       'packages',
@@ -464,6 +495,21 @@ class SingleExeBuild {
       'node_modules',
       'node-pty',
     )
+    if (target.platform === 'win') {
+      if (target.arch !== 'x64') {
+        throw new Error('build-exe-for-python-sdk: Windows supports x64 only.')
+      }
+      const host = Target.host()
+      if (target.platform !== host.platform || target.arch !== host.arch) {
+        throw new Error(
+          'build-exe-for-python-sdk: build the Windows runtime under x64 Node on its target host; '
+          + `target ${target.platform}-${target.arch} does not match host ${host.platform}-${host.arch}.`,
+        )
+      }
+      resolveWindowsNodePtyAddons(join(this.staging, 'node_modules', 'node-pty'), target.arch)
+      return
+    }
+    if (target.platform !== 'linux') return
     const destination = join(stagedBuild, 'Release', 'pty.node')
     const source = resolveLinuxNodePtyAddon(packageDirectory, target.arch)
     if (this.cli.dryRun) {
@@ -553,6 +599,12 @@ class SingleExeBuild {
       })
     })
   }
+
+  /** Run pnpm through its JavaScript entrypoint when the caller supplies one. */
+  private async runPnpm(label: string, args: string[]): Promise<void> {
+    const [command, invocationArgs] = pnpmInvocation(args)
+    await this.run(label, command, invocationArgs)
+  }
 }
 
 async function main(): Promise<void> {

+ 11 - 7
scripts/build-python-release.py

@@ -47,9 +47,12 @@ def load_platforms(path: Path = PLATFORM_MANIFEST) -> dict[str, tuple[str, str]]
 PLATFORMS = load_platforms()
 
 
-def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
-    suffixes = ("", "-rg")
-    return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
+def runtime_filenames(executable_name: str) -> tuple[str, ...]:
+    """Return the exact platform payload names for one runtime executable."""
+    if executable_name.endswith(".exe"):
+        return (executable_name, f"{executable_name.removesuffix('.exe')}-rg.exe")
+    names = (executable_name, f"{executable_name}-rg")
+    return (*names, f"{executable_name}-spawn-helper") if "-macos-" in executable_name else names
 
 
 def main() -> None:
@@ -210,8 +213,9 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
     rewrite_version(destination / "pyproject.toml", version)
     runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
     runtime_dir.mkdir(parents=True, exist_ok=True)
-    for suffix in runtime_suffixes(executable_name):
-        shutil.copy2(Path(f"{executable}{suffix}"), runtime_dir / f"{executable_name}{suffix}")
+    source_directory = executable.parent
+    for filename in runtime_filenames(executable_name):
+        shutil.copy2(source_directory / filename, runtime_dir / filename)
 
 
 def verify_wheel(
@@ -250,13 +254,13 @@ def verify_wheel(
         ]
         if package == "runtime":
             assert platform is not None
-            expected_files = [f"{platform[1]}{suffix}" for suffix in runtime_suffixes(platform[1])]
+            expected_files = sorted(runtime_filenames(platform[1]))
             found_files = sorted(Path(name).name for name in runtime_files)
             if found_files != expected_files:
                 raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
             for runtime_file in runtime_files:
                 mode = archive.getinfo(runtime_file).external_attr >> 16
-                if mode & stat.S_IXUSR == 0:
+                if platform[0] != "win_amd64" and mode & stat.S_IXUSR == 0:
                     raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {runtime_file}")
         elif runtime_files:
             raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")

+ 8 - 3
scripts/verify-runtime-closure.spec.ts

@@ -21,6 +21,7 @@ const platforms = {
   'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
   'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
   'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
+  'win-x64': { tag: 'win_amd64', executable: 'runtime-win-x64.exe' },
 }
 
 function workspace(root: string, name: string, manifest: Record<string, unknown>): void {
@@ -35,7 +36,7 @@ afterEach(() => {
 })
 
 describe('verifyRuntimeClosure', () => {
-  it('requires only plugins active for a Linux or macOS target', async () => {
+  it('requires only plugins active for each published target', async () => {
     const root = fixture({
       'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
       'python/sdk-runtime/platforms.json': platforms,
@@ -52,6 +53,9 @@ describe('verifyRuntimeClosure', () => {
     - id: macos
       name: '@scope/macos'
       disabled: !!js process.platform !== 'darwin'
+    - id: windows
+      name: '@scope/windows'
+      disabled: !!js process.platform !== 'win32'
 `,
     })
 
@@ -61,6 +65,7 @@ describe('verifyRuntimeClosure', () => {
     expect(result.failures).toEqual([
       'standard preset -> @scope/linux (linux-arm64, linux-x64)',
       'standard preset -> @scope/macos (macos-arm64)',
+      'standard preset -> @scope/windows (win-x64)',
     ])
   })
 
@@ -78,7 +83,7 @@ describe('verifyRuntimeClosure', () => {
     const result = await verifyRuntimeClosure(root)
 
     expect(result.failures).toEqual([
-      'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64)',
+      'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, win-x64)',
     ])
   })
 
@@ -112,7 +117,7 @@ describe('verifyRuntimeClosure', () => {
     const result = await verifyRuntimeClosure(root)
 
     expect(result.failures).toEqual([
-      'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64)',
+      'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, win-x64)',
     ])
   })
 

+ 1 - 0
scripts/verify-runtime-closure.ts

@@ -181,6 +181,7 @@ function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
 function processPlatformForTarget(target: string): string {
   if (target.startsWith('linux-')) return 'linux'
   if (target.startsWith('macos-')) return 'darwin'
+  if (target.startsWith('win-')) return 'win32'
   throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
 }