Przeglądaj źródła

single-exe: closure manifest, build pipeline, and CI workflow

imccyu 2 miesięcy temu
rodzic
commit
bd831db80b

+ 200 - 0
.github/workflows/build-exe-for-python-sdk.yml

@@ -0,0 +1,200 @@
+name: Build single-exe
+
+# Single-file executable (single-exe) builds of the DeepSeek Harness SDK
+# runtime. The build pipeline and target platforms are specified in
+# docs/rfc/implemented/architecture/2026-07-10-single-exe-sdk-runtime.md: each target is
+# built natively on a runner of its own platform (no cross-compilation) by
+# scripts/build-exe-for-python-sdk.ts, which deploys the dsh-jsonrpc-agent-pkg closure manifest
+# closure with @yao-pkg/pkg into dist-exe/.
+#
+# Each build leg uploads two artifacts:
+#   - dsh-jsonrpc-agent-pkg-<target> — the bare single-file exe, for
+#     consumers that want just the binary.
+#   - deepseek-harness-python-<target> — the whole python/ directory as a
+#     tar.gz with that exe already embedded (the build script syncs it into
+#     the Python runtime package): unpack and both packages pip install
+#     as-is, the checked-in default runtime/cordis.yml is editable in
+#     place, and the embedded exe also runs directly.
+#
+# workflow_dispatch ONLY — deliberately not triggered by push/pull_request:
+# the exe is a release-style deliverable, and the build (full pnpm build +
+# pnpm deploy + pkg across a 3-platform matrix, ~100MB per artifact) is far
+# too expensive to run as a per-commit CI signal. Dispatch it from the
+# Actions tab when artifacts are needed. There is no `ref` input on purpose:
+# actions/checkout already checks out the branch/tag the run was dispatched
+# on.
+on:
+  workflow_dispatch:
+    inputs:
+      targets:
+        description: >-
+          Comma-separated pkg targets to build. Any subset of:
+          node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
+        type: string
+        required: false
+        default: node24-linux-x64,node24-linux-arm64,node24-macos-arm64
+
+# Manual runs on the same ref supersede each other.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+# Least privilege: the jobs only read the repo; artifact upload needs no
+# extra scope.
+permissions:
+  contents: read
+
+jobs:
+  # Turn the `targets` input into the build matrix. The `matrix` context is
+  # not available in a job-level `if:` (jobs.<job_id>.if only sees
+  # github/needs/vars/inputs), so target selection happens here instead of
+  # skipping matrix legs; an unknown target name fails the whole run loudly
+  # instead of being silently ignored.
+  plan:
+    name: plan targets
+    runs-on: ubuntu-latest
+    timeout-minutes: 5
+    outputs:
+      matrix: ${{ steps.plan.outputs.matrix }}
+    steps:
+      - name: Compute matrix from targets input
+        id: plan
+        env:
+          TARGETS: ${{ inputs.targets }}
+        run: |
+          set -euo pipefail
+          matrix='[]'
+          IFS=',' read -r -a targets <<< "$TARGETS"
+          for raw in "${targets[@]}"; do
+            t="$(echo "$raw" | xargs)" # trim surrounding whitespace
+            [ -z "$t" ] && continue
+            # Native builds only — each target maps to a runner of its own
+            # platform: linux-arm64 uses GitHub's hosted arm64 label
+            # ubuntu-24.04-arm (there is no ubuntu-latest-arm), macos-arm64
+            # uses macos-latest (Apple Silicon since macos-14).
+            case "$t" in
+              node24-linux-x64)   runner=ubuntu-latest ;;
+              node24-linux-arm64) runner=ubuntu-24.04-arm ;;
+              node24-macos-arm64) runner=macos-latest ;;
+              *)
+                echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
+                exit 1
+                ;;
+            esac
+            matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
+          done
+          if [ "$matrix" = '[]' ]; then
+            echo "::error::The targets input selected nothing to build."
+            exit 1
+          fi
+          echo "Matrix: $matrix"
+          echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
+
+  build:
+    needs: plan
+    name: ${{ matrix.target }}
+    runs-on: ${{ matrix.runner }}
+    timeout-minutes: 45
+    strategy:
+      fail-fast: false
+      matrix:
+        include: ${{ fromJSON(needs.plan.outputs.matrix) }}
+    steps:
+      - uses: actions/checkout@v6
+
+      - uses: actions/setup-node@v6
+        with:
+          node-version: 24
+
+      - name: Enable corepack (pnpm)
+        run: corepack enable
+
+      - name: Resolve pnpm store path
+        id: pnpm-store
+        run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
+
+      # Unlike ci.yml (x64-only), this matrix spans two Linux architectures
+      # that share runner.os, so runner.arch is part of the key.
+      - uses: actions/cache@v4
+        with:
+          path: ${{ steps.pnpm-store.outputs.path }}
+          key: ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
+          restore-keys: |
+            ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-
+
+      # The first run per target has pkg-fetch download yao-pkg's patched
+      # Node binary into ~/.pkg-cache; cache it so later runs skip the
+      # download. The target string pins Node major + platform + arch;
+      # pnpm-lock.yaml rolls the key when @yao-pkg/pkg (and with it the
+      # pinned patched-binary version) is bumped, with restore-keys still
+      # seeding from the previous cache.
+      - uses: actions/cache@v4
+        with:
+          path: ~/.pkg-cache
+          key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
+          restore-keys: |
+            pkg-fetch-${{ matrix.target }}-
+
+      - name: Install (immutable)
+        run: pnpm install --frozen-lockfile
+
+      # The script runs the whole pipeline itself (pnpm run build → pnpm
+      # deploy --prod → pkg) and writes its output to dist-exe/ by default.
+      - name: Build single-exe
+        run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: dsh-jsonrpc-agent-pkg-${{ matrix.target }}
+          path: dist-exe/
+          if-no-files-found: error
+
+      # After the build step, python/ is already in its complete
+      # distributable shape — the script synced this leg's exe into
+      # python/sdk-runtime/src/deepseek_harness_runtime/runtime/ next to the
+      # checked-in default cordis.yml — so packing is all that is left.
+      # Everything goes under one top-level deepseek-harness-python/
+      # directory so unpacking never scatters files. Shipping a tar (rather
+      # than uploading the tree bare) preserves the exe's executable bit —
+      # tar keeps file modes; upload-artifact's zip does not.
+      #
+      # Excluded: runtime/node/ (dev-only node-mode carrier, ~140MB) plus
+      # __pycache__ / .pytest_cache / .venv / node_modules anywhere (install
+      # or test leftovers); uv.lock stays in.
+      #
+      # Portability: GNU tar (ubuntu) and bsdtar (macos) both accept
+      # `tar -czf out.tar.gz --exclude=… -C <parent> <dir>` and both treat
+      # an excluded directory as pruned (no descent). The top-level rename
+      # is done by copying into a temp dir first — GNU --transform / BSD -s
+      # are single-implementation flags.
+      - name: Pack Python SDK bundle
+        id: pack
+        env:
+          TARGET: ${{ matrix.target }}
+        run: |
+          set -euo pipefail
+          platform_arch="${TARGET#*-}" # node24-macos-arm64 -> macos-arm64
+          exe="python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-${platform_arch}"
+          if [ ! -x "$exe" ]; then
+            echo "::error::$exe missing or not executable — the build step did not sync this leg's exe into the Python runtime package; refusing to pack a half-empty bundle."
+            exit 1
+          fi
+          staging="$(mktemp -d)"
+          cp -R python "$staging/deepseek-harness-python"
+          bundle="deepseek-harness-python-${platform_arch}.tar.gz"
+          tar -czf "$bundle" \
+            --exclude='deepseek-harness-python/sdk-runtime/src/deepseek_harness_runtime/runtime/node' \
+            --exclude='__pycache__' \
+            --exclude='.pytest_cache' \
+            --exclude='.venv' \
+            --exclude='node_modules' \
+            -C "$staging" deepseek-harness-python
+          rm -rf "$staging"
+          ls -lh "$bundle"
+          echo "bundle=$bundle" >> "$GITHUB_OUTPUT"
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: deepseek-harness-python-${{ matrix.target }}
+          path: ${{ steps.pack.outputs.bundle }}
+          if-no-files-found: error

+ 3 - 0
.gitignore

@@ -19,5 +19,8 @@ tmp/
 .DS_Store
 .idea
 mise.toml
+dist-exe/
+python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*
+python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/
 python/**/__pycache__/
 python/**/.pytest_cache/

+ 1 - 1
knip.json

@@ -2,7 +2,7 @@
   "$schema": "https://unpkg.com/knip@5/schema.json",
   "exclude": ["duplicates"],
   "ignoreBinaries": ["bwrap", "sandbox-exec"],
-  "ignoreWorkspaces": ["vendor/*"],
+  "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime"],
   "workspaces": {
     ".": {
       "entry": [

+ 180 - 0
pnpm-lock.yaml

@@ -1620,6 +1620,186 @@ importers:
         specifier: ^4.19.2
         version: 4.22.4
 
+  python/sdk-runtime:
+    dependencies:
+      '@cordisjs/plugin-include':
+        specifier: workspace:^
+        version: link:../../vendor/include
+      '@cordisjs/plugin-loader':
+        specifier: workspace:^
+        version: link:../../vendor/loader
+      '@cordisjs/plugin-timer':
+        specifier: workspace:^
+        version: link:../../vendor/timer
+      '@deepseek-ai/dsh-acp':
+        specifier: workspace:^
+        version: link:../../packages/ui/acp
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../packages/core/agent
+      '@deepseek-ai/dsh-agent-core':
+        specifier: workspace:^
+        version: link:../../packages/core/agent-core
+      '@deepseek-ai/dsh-agent-loop':
+        specifier: workspace:^
+        version: link:../../packages/core/agent-loop
+      '@deepseek-ai/dsh-app-boot':
+        specifier: workspace:^
+        version: link:../../packages/ui/app-boot
+      '@deepseek-ai/dsh-bash':
+        specifier: workspace:^
+        version: link:../../packages/bash/bash
+      '@deepseek-ai/dsh-bash-local':
+        specifier: workspace:^
+        version: link:../../packages/bash/bash-local
+      '@deepseek-ai/dsh-brand':
+        specifier: workspace:^
+        version: link:../../packages/util/brand
+      '@deepseek-ai/dsh-code-runtime':
+        specifier: workspace:^
+        version: link:../../packages/code-runtime/code-runtime
+      '@deepseek-ai/dsh-code-runtime-worker':
+        specifier: workspace:^
+        version: link:../../packages/code-runtime/code-runtime-worker
+      '@deepseek-ai/dsh-compact':
+        specifier: workspace:^
+        version: link:../../packages/compact/compact
+      '@deepseek-ai/dsh-compact-basic':
+        specifier: workspace:^
+        version: link:../../packages/compact/compact-basic
+      '@deepseek-ai/dsh-fs':
+        specifier: workspace:^
+        version: link:../../packages/fs/fs
+      '@deepseek-ai/dsh-fs-local':
+        specifier: workspace:^
+        version: link:../../packages/fs/fs-local
+      '@deepseek-ai/dsh-fs-policy':
+        specifier: workspace:^
+        version: link:../../packages/fs/fs-policy
+      '@deepseek-ai/dsh-hook-protocol':
+        specifier: workspace:^
+        version: link:../../packages/hooks/hook-protocol
+      '@deepseek-ai/dsh-hooks-claude':
+        specifier: workspace:^
+        version: link:../../packages/hooks/hooks-claude
+      '@deepseek-ai/dsh-hooks-codex':
+        specifier: workspace:^
+        version: link:../../packages/hooks/hooks-codex
+      '@deepseek-ai/dsh-invariants':
+        specifier: workspace:^
+        version: link:../../packages/support/invariants
+      '@deepseek-ai/dsh-jsonrpc':
+        specifier: workspace:^
+        version: link:../../packages/ui/jsonrpc
+      '@deepseek-ai/dsh-jsonrpc-agent':
+        specifier: workspace:^
+        version: link:../../packages/ui/jsonrpc-agent
+      '@deepseek-ai/dsh-llm':
+        specifier: workspace:^
+        version: link:../../packages/llm/llm
+      '@deepseek-ai/dsh-llm-deepseek':
+        specifier: workspace:^
+        version: link:../../packages/llm/llm-deepseek
+      '@deepseek-ai/dsh-llm-pi-ai':
+        specifier: workspace:^
+        version: link:../../packages/llm/llm-pi-ai
+      '@deepseek-ai/dsh-repeat-tool-guard':
+        specifier: workspace:^
+        version: link:../../packages/guard/repeat-tool-guard
+      '@deepseek-ai/dsh-session':
+        specifier: workspace:^
+        version: link:../../packages/core/session
+      '@deepseek-ai/dsh-session-persistence':
+        specifier: workspace:^
+        version: link:../../packages/session-persistence/session-persistence
+      '@deepseek-ai/dsh-session-persistence-jsonl':
+        specifier: workspace:^
+        version: link:../../packages/session-persistence/session-persistence-jsonl
+      '@deepseek-ai/dsh-session-persistence-sqlite':
+        specifier: workspace:^
+        version: link:../../packages/session-persistence/session-persistence-sqlite
+      '@deepseek-ai/dsh-subagent':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent
+      '@deepseek-ai/dsh-subagent-acp':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent-acp
+      '@deepseek-ai/dsh-subagent-fork':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent-fork
+      '@deepseek-ai/dsh-subagent-inprocess':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent-inprocess
+      '@deepseek-ai/dsh-subagent-spawn':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent-spawn
+      '@deepseek-ai/dsh-subagent-subprocess':
+        specifier: workspace:^
+        version: link:../../packages/subagent/subagent-subprocess
+      '@deepseek-ai/dsh-system-prompt':
+        specifier: workspace:^
+        version: link:../../packages/core/system-prompt
+      '@deepseek-ai/dsh-timeout':
+        specifier: workspace:^
+        version: link:../../packages/util/timeout
+      '@deepseek-ai/dsh-timeout-policy':
+        specifier: workspace:^
+        version: link:../../packages/timeout/timeout-policy
+      '@deepseek-ai/dsh-tool-ask-user':
+        specifier: workspace:^
+        version: link:../../packages/ui/tool-ask-user
+      '@deepseek-ai/dsh-tool-bash':
+        specifier: workspace:^
+        version: link:../../packages/bash/tool-bash
+      '@deepseek-ai/dsh-tool-cordis':
+        specifier: workspace:^
+        version: link:../../packages/cordis/tool-cordis
+      '@deepseek-ai/dsh-tool-fs':
+        specifier: workspace:^
+        version: link:../../packages/fs/tool-fs
+      '@deepseek-ai/dsh-tool-subagent':
+        specifier: workspace:^
+        version: link:../../packages/subagent/tool-subagent
+      '@deepseek-ai/dsh-tool-todo':
+        specifier: workspace:^
+        version: link:../../packages/todo/tool-todo
+      '@deepseek-ai/dsh-tool-web':
+        specifier: workspace:^
+        version: link:../../packages/web/tool-web
+      '@deepseek-ai/dsh-tool-workflow':
+        specifier: workspace:^
+        version: link:../../packages/workflow/tool-workflow
+      '@deepseek-ai/dsh-tools':
+        specifier: workspace:^
+        version: link:../../packages/core/tools
+      '@deepseek-ai/dsh-user-interaction':
+        specifier: workspace:^
+        version: link:../../packages/ui/user-interaction
+      '@deepseek-ai/dsh-web':
+        specifier: workspace:^
+        version: link:../../packages/web/web
+      '@deepseek-ai/dsh-web-fetch-local':
+        specifier: workspace:^
+        version: link:../../packages/web/web-fetch-local
+      '@deepseek-ai/dsh-web-search-deepseek':
+        specifier: workspace:^
+        version: link:../../packages/web/web-search-deepseek
+      '@deepseek-ai/dsh-web-search-exa':
+        specifier: workspace:^
+        version: link:../../packages/web/web-search-exa
+      '@deepseek-ai/dsh-web-search-perplexity':
+        specifier: workspace:^
+        version: link:../../packages/web/web-search-perplexity
+      '@deepseek-ai/dsh-workflow':
+        specifier: workspace:^
+        version: link:../../packages/workflow/workflow
+      '@deepseek-ai/dsh-workflow-workerthread':
+        specifier: workspace:^
+        version: link:../../packages/workflow/workflow-workerthread
+      cordis:
+        specifier: workspace:^
+        version: link:../../vendor/cordis
+
   vendor/cordis:
     dependencies:
       '@cordisjs/plugin-include':

+ 3 - 0
pnpm-workspace.yaml

@@ -1,6 +1,9 @@
 packages:
   - vendor/*
   - packages/*/*
+  # Deploy root of the single-exe build: a pure dependency manifest whose
+  # closure is what the exe bundles and what the Python runtime distributes.
+  - python/sdk-runtime
 
 peerDependencyRules:
   allowedVersions:

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

@@ -0,0 +1,68 @@
+{
+  "name": "dsh-jsonrpc-agent-pkg",
+  "description": "Deploy root of the single-exe pipeline and the single source of truth unifying 'which plugins the exe bundles' and 'what the Python runtime distributes': the dependency list below IS the exe closure. Pure manifest — no code; a deploy materializes only this package.json plus node_modules.",
+  "version": "0.0.1",
+  "private": true,
+  "type": "module",
+  "dependencies": {
+    "@cordisjs/plugin-include": "workspace:^",
+    "@cordisjs/plugin-loader": "workspace:^",
+    "@cordisjs/plugin-timer": "workspace:^",
+    "@deepseek-ai/dsh-acp": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-agent-core": "workspace:^",
+    "@deepseek-ai/dsh-agent-loop": "workspace:^",
+    "@deepseek-ai/dsh-app-boot": "workspace:^",
+    "@deepseek-ai/dsh-bash": "workspace:^",
+    "@deepseek-ai/dsh-bash-local": "workspace:^",
+    "@deepseek-ai/dsh-brand": "workspace:^",
+    "@deepseek-ai/dsh-code-runtime": "workspace:^",
+    "@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
+    "@deepseek-ai/dsh-compact": "workspace:^",
+    "@deepseek-ai/dsh-compact-basic": "workspace:^",
+    "@deepseek-ai/dsh-fs": "workspace:^",
+    "@deepseek-ai/dsh-fs-local": "workspace:^",
+    "@deepseek-ai/dsh-fs-policy": "workspace:^",
+    "@deepseek-ai/dsh-hook-protocol": "workspace:^",
+    "@deepseek-ai/dsh-hooks-claude": "workspace:^",
+    "@deepseek-ai/dsh-hooks-codex": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-jsonrpc": "workspace:^",
+    "@deepseek-ai/dsh-jsonrpc-agent": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
+    "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
+    "@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-persistence": "workspace:^",
+    "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+    "@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
+    "@deepseek-ai/dsh-subagent": "workspace:^",
+    "@deepseek-ai/dsh-subagent-acp": "workspace:^",
+    "@deepseek-ai/dsh-subagent-fork": "workspace:^",
+    "@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
+    "@deepseek-ai/dsh-subagent-spawn": "workspace:^",
+    "@deepseek-ai/dsh-subagent-subprocess": "workspace:^",
+    "@deepseek-ai/dsh-system-prompt": "workspace:^",
+    "@deepseek-ai/dsh-timeout": "workspace:^",
+    "@deepseek-ai/dsh-timeout-policy": "workspace:^",
+    "@deepseek-ai/dsh-tool-ask-user": "workspace:^",
+    "@deepseek-ai/dsh-tool-bash": "workspace:^",
+    "@deepseek-ai/dsh-tool-cordis": "workspace:^",
+    "@deepseek-ai/dsh-tool-fs": "workspace:^",
+    "@deepseek-ai/dsh-tool-subagent": "workspace:^",
+    "@deepseek-ai/dsh-tool-todo": "workspace:^",
+    "@deepseek-ai/dsh-tool-web": "workspace:^",
+    "@deepseek-ai/dsh-tool-workflow": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^",
+    "@deepseek-ai/dsh-user-interaction": "workspace:^",
+    "@deepseek-ai/dsh-web": "workspace:^",
+    "@deepseek-ai/dsh-web-fetch-local": "workspace:^",
+    "@deepseek-ai/dsh-web-search-deepseek": "workspace:^",
+    "@deepseek-ai/dsh-web-search-exa": "workspace:^",
+    "@deepseek-ai/dsh-web-search-perplexity": "workspace:^",
+    "@deepseek-ai/dsh-workflow": "workspace:^",
+    "@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
+    "cordis": "workspace:^"
+  }
+}

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

@@ -0,0 +1,457 @@
+/**
+ * Build the single-file SDK runtime executables
+ * (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
+ *
+ * Every settled decision is hardcoded — the PoC judged @yao-pkg/pkg's
+ * standard mode unusable for this architecture (its ESM→CJS transform breaks
+ * every runtime `import()`), so the pipeline is fixed on `--sea` mode, plain
+ * ESM entry, plain-source assets, and a hoisted (symlink-free) staged tree.
+ *
+ * Pipeline — every step fails loud with the command it ran:
+ *
+ *   1. `pnpm run build` — all packages emit `lib/` (skippable via --skip-build).
+ *   2. `pnpm --filter dsh-jsonrpc-agent-pkg deploy` — materialize the
+ *      closure-manifest package (python/sdk-runtime/package.json — the single
+ *      source of truth for the exe's plugin set) into the staging dir
+ *      (cleared first; pnpm refuses a non-empty deploy target). Flags, all
+ *      verified against pnpm 11.7: `--legacy` because the workspace does not
+ *      set `inject-workspace-packages=true`; `node-linker=hoisted` for a plain
+ *      file tree with zero symlinks (the safe shape for pkg's VFS, and it
+ *      physically guarantees a single cordis copy); `auto-install-peers=false`
+ *      so transitive `^0.0.x` peers on unpublished packages never hit the
+ *      registry; `link-workspace-packages=true` so the closure resolves to
+ *      workspace/vendor sources.
+ *   3. Inject the pkg config into the staged package.json: `bin` = the ESM
+ *      `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` (SEA mode
+ *      hands it to Node's default ESM loader — no CJS shim), plus whole-tree
+ *      asset globs. The cordis Loader resolves plugins
+ *      through runtime dynamic `import()` of bare package names, so pkg's
+ *      static analysis discovers none of them — the entire staged tree must be
+ *      globbed in explicitly.
+ *   4. `pnpm dlx @yao-pkg/pkg@<pinned> <staging> --sea --targets <t> --output
+ *      <out>/dsh-jsonrpc-agent-pkg-<platform>-<arch>` — once per target (SEA mode
+ *      packs a single target per invocation), so each product gets its
+ *      canonical name directly.
+ *   5. Sync into the Python runtime package
+ *      (python/sdk-runtime/src/deepseek_harness_runtime/runtime/,
+ *      created if missing): each product under its canonical filename (exe
+ *      mode), plus the whole staged closure into runtime/node/ (node mode —
+ *      `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`
+ *      runs it directly; the injected pkg
+ *      fields are harmless to node). dist-exe/ keeps the originals for CI
+ *      artifact upload.
+ *
+ *   `pnpm exec tsx scripts/build-exe-for-python-sdk.ts`            → host-platform exe into dist-exe/
+ *   `pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64`
+ *   `pnpm exec tsx scripts/build-exe-for-python-sdk.ts --dry-run`  → print the plan without executing
+ */
+
+import { spawn } from 'node:child_process'
+import { existsSync, mkdirSync, statSync } from 'node:fs'
+import { copyFile, readFile, rm, writeFile } from 'node:fs/promises'
+import { basename, join, resolve, sep } from 'node:path'
+import { parseArgs } from 'node:util'
+
+const root = resolve(import.meta.dirname, '..')
+
+/**
+ * The deploy root: the closure-manifest package (python/sdk-runtime) whose
+ * dependencies define the exe's contents; the runnable entry inside the
+ * closure is {@link ENTRY_BIN}.
+ */
+const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
+/** The bin entry inside the deployed closure (the dsh-jsonrpc-agent app bin). */
+const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js'
+/** Basename of every product; the canonical name appends `-<platform>-<arch>`. */
+const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
+/** Default exe Node major; SEA mode requires >= node22, the repo tracks node24. */
+const DEFAULT_NODE_RANGE = 'node24'
+/** Pinned pkg version (the one the PoC and acceptance ran on) for reproducible builds. */
+const PKG_SPEC = '@yao-pkg/pkg@6.21.0'
+/** Staging dir for the deployed closure — cleared on every run (gitignored). */
+// (No external staging dir: the deploy target IS the Python runtime's
+// node-mode carrier — see PYTHON_RUNTIME_DIR/PYTHON_NODE_SUBDIR.)
+/** Product output dir (gitignored). */
+const OUT_DIR = 'dist-exe'
+/**
+ * Python runtime package dir the products are synced into. A parallel change
+ * owns the directory and its .gitignore; this script's only contract is the
+ * destination path, so a missing dir is created, never an error.
+ */
+const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
+/** Subdir of {@link PYTHON_RUNTIME_DIR} carrying the staged closure for node-mode execution. */
+const PYTHON_NODE_SUBDIR = 'node'
+
+/**
+ * Whole-tree asset globs. The cordis Loader dynamic-imports bare package names
+ * at runtime, invisible to pkg's static analysis, so every runtime file in the
+ * closure is listed; SEA mode ships them as plain source in the VFS. Every
+ * package.json must ride along — bare-name resolution dies without them (the
+ * json glob would already match, but the manifests are resolution-critical, so
+ * they get their own explicit entry).
+ */
+const ASSET_GLOBS = [
+  'package.json',
+  'node_modules/**/*.js',
+  'node_modules/**/*.cjs',
+  'node_modules/**/*.mjs',
+  'node_modules/**/package.json',
+  'node_modules/**/*.json',
+  'node_modules/**/*.node',
+  'node_modules/**/*.wasm',
+]
+
+const PLATFORMS = ['linux', 'macos'] as const
+const ARCHES = ['x64', 'arm64'] as const
+type Platform = (typeof PLATFORMS)[number]
+type Arch = (typeof ARCHES)[number]
+
+/** True when `value` is a supported pkg platform tag. */
+function isPlatform(value: string): value is Platform {
+  return (PLATFORMS as readonly string[]).includes(value)
+}
+
+/** True when `value` is a supported pkg CPU tag. */
+function isArch(value: string): value is Arch {
+  return (ARCHES as readonly string[]).includes(value)
+}
+
+/**
+ * One pkg target triple, e.g. `node24-linux-x64`, as an immutable value.
+ * Construction goes through {@link Target.parse} (a `--targets` entry) or
+ * {@link Target.host} (the default), which own all validation.
+ */
+class Target {
+  private constructor(
+    /** pkg Node range (`node<major>`); pins the official base binary pkg pulls. */
+    readonly nodeRange: string,
+    /**
+     * pkg platform tag. Windows is a documented non-goal
+     * (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
+     */
+    readonly platform: Platform,
+    /** pkg CPU tag. */
+    readonly arch: Arch,
+  ) {}
+
+  /** The pkg `--targets` spec string `<nodeRange>-<platform>-<arch>`. */
+  get spec(): string {
+    return `${this.nodeRange}-${this.platform}-${this.arch}`
+  }
+
+  /**
+   * Parse and validate one target spec; throws on any malformed component.
+   * @param spec - the raw triple, e.g. `node24-linux-x64`.
+   * @returns the parsed target.
+   */
+  static parse(spec: string): Target {
+    const parts = spec.split('-')
+    const [nodeRange, platform, arch] = parts
+    if (parts.length !== 3 || nodeRange === undefined || platform === undefined || arch === undefined) {
+      throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)} must be <nodeRange>-<platform>-<arch>, e.g. node24-linux-x64.`)
+    }
+    if (!/^node\d+$/.test(nodeRange)) {
+      throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: node range must look like node24, got ${JSON.stringify(nodeRange)}.`)
+    }
+    if (!isPlatform(platform)) {
+      throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: platform must be one of ${PLATFORMS.join(', ')} (Windows is a docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md non-goal), got ${JSON.stringify(platform)}.`)
+    }
+    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)}.`)
+    }
+    return new Target(nodeRange, platform, arch)
+  }
+
+  /**
+   * The default target when --targets is omitted: the host platform on node24.
+   * @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
+    if (platform === undefined) {
+      throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
+    }
+    const arch = process.arch === 'x64' || process.arch === 'arm64' ? process.arch : undefined
+    if (arch === undefined) {
+      throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
+    }
+    return new Target(DEFAULT_NODE_RANGE, platform, arch)
+  }
+}
+
+/**
+ * Parsed CLI configuration. {@link BuildCli.parse} is the only constructor
+ * path — it owns flag parsing, target validation, and the --help / bad-flag
+ * process exits, so an instance always holds a valid plan.
+ */
+class BuildCli {
+  private constructor(
+    /** Build targets; defaults to the host platform only. */
+    readonly targets: readonly Target[],
+    /** Skip step 1 (`pnpm run build`); lib/ artifacts must already exist. */
+    readonly skipBuild: boolean,
+    /** Print every command and config patch instead of executing. */
+    readonly dryRun: boolean,
+  ) {}
+
+  /**
+   * Parse argv into a validated configuration. Exits the process for --help
+   * (code 0, usage) and for unknown/malformed flags (code 1, usage on
+   * stderr); throws on invalid or colliding targets.
+   * @param argv - the raw arguments (`process.argv.slice(2)`).
+   * @returns the parsed, validated configuration.
+   */
+  static parse(argv: string[]): BuildCli {
+    let values: ReturnType<typeof BuildCli.parseRaw>
+    try {
+      values = BuildCli.parseRaw(argv)
+    } catch (error) {
+      console.error(`build-exe-for-python-sdk: ${error instanceof Error ? error.message : String(error)}\n`)
+      console.error(BuildCli.usage())
+      process.exit(1)
+    }
+    if (values.help) {
+      console.log(BuildCli.usage())
+      process.exit(0)
+    }
+    const targets = values.targets === undefined
+      ? [Target.host()]
+      : values.targets.split(',').map(part => part.trim()).filter(part => part !== '').map(spec => Target.parse(spec))
+    if (targets.length === 0) throw new Error('build-exe-for-python-sdk: --targets is empty.')
+    const seen = new Set<string>()
+    for (const target of targets) {
+      const key = `${target.platform}-${target.arch}`
+      if (seen.has(key)) {
+        throw new Error(`build-exe-for-python-sdk: duplicate platform-arch ${key} in --targets; canonical product names would collide.`)
+      }
+      seen.add(key)
+    }
+    return new BuildCli(targets, values['skip-build'], values['dry-run'])
+  }
+
+  /** The flag grammar in one place; parseArgs throws on any unknown flag. */
+  private static parseRaw(argv: string[]) {
+    return parseArgs({
+      args: argv,
+      options: {
+        'targets': { type: 'string' },
+        'skip-build': { type: 'boolean', default: false },
+        'dry-run': { type: 'boolean', default: false },
+        'help': { type: 'boolean', default: false },
+      },
+    }).values
+  }
+
+  /** The --help text; also printed under flag-parse errors. */
+  private static usage(): string {
+    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.',
+      '                         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.',
+      '  --help                 print this help.',
+      '',
+      'Settled decisions are hardcoded (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md): pkg runs in --sea mode',
+      `(standard mode breaks runtime import()), pinned to ${PKG_SPEC}; the deploy tree is`,
+      `hoisted/symlink-free; the closure deploys straight into ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and products land in ${OUT_DIR}/.`,
+    ].join('\n')
+  }
+}
+
+/** The pnpm executable name for the host OS. */
+function pnpmBin(): string {
+  return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
+}
+
+/**
+ * Render a command line for logs and error messages, quoting arguments that
+ * contain spaces.
+ * @param command - the executable.
+ * @param args - its arguments.
+ * @returns the printable command line.
+ */
+function formatCommand(command: string, args: string[]): string {
+  return [command, ...args].map(part => (part.includes(' ') ? JSON.stringify(part) : part)).join(' ')
+}
+
+/**
+ * The four-step build pipeline over one parsed CLI. Steps are sequential
+ * async methods; every subprocess inherits stdio and fails loud with the
+ * exact command it ran. In --dry-run the command/filesystem layer prints
+ * what it would do instead of executing.
+ */
+class SingleExeBuild {
+  /**
+   * Absolute staging dir — the Python runtime's node-mode carrier: step 2
+   * deploys the closure DIRECTLY here (cleared first; it is a pure build
+   * product, the checked-in default `cordis.yml` lives one level up), step 4
+   * reads it as the pkg input, and node mode runs it in place.
+   */
+  readonly staging = resolve(root, PYTHON_RUNTIME_DIR, PYTHON_NODE_SUBDIR)
+  /** Absolute product output dir. */
+  private readonly outDir = resolve(root, OUT_DIR)
+
+  constructor(private readonly cli: BuildCli) {}
+
+  /** Step 1: `pnpm run build` — all packages emit `lib/` (skipped via --skip-build). */
+  async build(): Promise<void> {
+    if (this.cli.skipBuild) {
+      console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
+      return
+    }
+    await this.run('build', pnpmBin(), ['run', 'build'])
+  }
+
+  /** Step 2: clear the staging dir and deploy the bridge closure into it. */
+  async deployStaging(): Promise<void> {
+    if (this.staging === root || root.startsWith(this.staging + sep)) {
+      throw new Error(`build-exe-for-python-sdk: refusing to clear staging dir ${this.staging}: it contains the repo root.`)
+    }
+    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(), [
+      '--filter',
+      DEPLOY_ROOT_PACKAGE,
+      'deploy',
+      '--legacy',
+      '--prod',
+      '--config.node-linker=hoisted',
+      '--config.auto-install-peers=false',
+      '--config.link-workspace-packages=true',
+      this.staging,
+    ])
+  }
+
+  /** Step 3: patch the staged package.json with the bin entry + pkg asset globs. */
+  async injectPkgConfig(): Promise<void> {
+    const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS } }
+    const manifestPath = join(this.staging, 'package.json')
+    if (this.cli.dryRun) {
+      console.log(`build-exe-for-python-sdk: [dry-run] patch ${manifestPath} with ${JSON.stringify(patch)}`)
+      return
+    }
+    if (!existsSync(manifestPath)) {
+      throw new Error(`build-exe-for-python-sdk: ${manifestPath} missing — pnpm deploy did not produce a staged package.`)
+    }
+    if (!existsSync(join(this.staging, ENTRY_BIN))) {
+      throw new Error(`build-exe-for-python-sdk: ${join(this.staging, ENTRY_BIN)} missing — run without --skip-build so lib/ artifacts exist.`)
+    }
+    const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<string, unknown>
+    await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
+    console.log(`build-exe-for-python-sdk: injected pkg config into ${manifestPath}`)
+  }
+
+  /**
+   * Step 4: run @yao-pkg/pkg over the staged tree for ONE target (SEA mode
+   * packs a single target per invocation) and return the product path.
+   * @param target - the pkg target triple to build.
+   * @returns the canonical product path `<out>/dsh-jsonrpc-agent-pkg-<platform>-<arch>`.
+   */
+  async pack(target: Target): Promise<string> {
+    const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
+    if (!this.cli.dryRun) mkdirSync(this.outDir, { recursive: true })
+    await this.run(`pkg ${target.spec}`, pnpmBin(), [
+      'dlx',
+      PKG_SPEC,
+      this.staging,
+      '--sea',
+      '--targets',
+      target.spec,
+      '--output',
+      product,
+    ])
+    if (!this.cli.dryRun && !existsSync(product)) {
+      throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
+    }
+    return product
+  }
+
+  /**
+   * Print each product path (and size, when it exists on disk).
+   * @param products - the product paths returned by {@link pack}.
+   */
+  printProducts(products: string[]): void {
+    console.log(this.cli.dryRun ? 'build-exe-for-python-sdk: [dry-run] would produce:' : 'build-exe-for-python-sdk: products:')
+    for (const product of products) {
+      if (this.cli.dryRun) {
+        console.log(`  ${product}`)
+        continue
+      }
+      const megabytes = statSync(product).size / (1024 * 1024)
+      console.log(`  ${product}  (${megabytes.toFixed(1)} MB)`)
+    }
+  }
+
+  /**
+   * Step 5: copy every product into the Python runtime package under its
+   * canonical filename (exe mode). The node-mode carrier needs no sync — step
+   * 2 deployed the closure into it directly. dist-exe/ keeps the originals
+   * for CI artifact upload; the destination dir is created if missing.
+   * @param products - the product paths returned by {@link pack}.
+   */
+  async syncToPythonRuntime(products: string[]): Promise<void> {
+    const destDir = resolve(root, PYTHON_RUNTIME_DIR)
+    if (this.cli.dryRun) {
+      for (const product of products) {
+        console.log(`build-exe-for-python-sdk: [dry-run] cp ${product} ${join(destDir, basename(product))}`)
+      }
+      return
+    }
+    mkdirSync(destDir, { recursive: true })
+    for (const product of products) {
+      const destination = join(destDir, basename(product))
+      await copyFile(product, destination)
+      console.log(`build-exe-for-python-sdk: synced ${destination}`)
+    }
+  }
+
+  /**
+   * Run one pipeline step as a subprocess with inherited stdio; reject —
+   * carrying the printable command — on spawn failure and non-zero exit
+   * alike. In --dry-run, print the command instead of executing.
+   * @param label - the step name used in logs and error messages.
+   * @param command - the executable.
+   * @param args - its arguments.
+   */
+  private async run(label: string, command: string, args: string[]): Promise<void> {
+    const printable = formatCommand(command, args)
+    if (this.cli.dryRun) {
+      console.log(`build-exe-for-python-sdk: [dry-run] ${printable}`)
+      return
+    }
+    console.log(`build-exe-for-python-sdk: ${label}: ${printable}`)
+    await new Promise<void>((resolvePromise, reject) => {
+      const child = spawn(command, args, { cwd: root, stdio: 'inherit' })
+      child.once('error', (error) => {
+        reject(new Error(`build-exe-for-python-sdk: ${label} failed to spawn: ${error.message} (${printable})`))
+      })
+      child.once('exit', (code, signal) => {
+        if (code === 0) {
+          resolvePromise()
+          return
+        }
+        const cause = code === null ? `signal ${signal ?? 'unknown'}` : `exit code ${code}`
+        reject(new Error(`build-exe-for-python-sdk: ${label} failed (${cause}): ${printable}`))
+      })
+    })
+  }
+}
+
+/** Entry point: parse the CLI, then await each pipeline step in order. */
+async function main(): Promise<void> {
+  const cli = BuildCli.parse(process.argv.slice(2))
+  const pipeline = new SingleExeBuild(cli)
+  console.log(`build-exe-for-python-sdk: targets: ${cli.targets.map(target => target.spec).join(', ')}`)
+  console.log(`build-exe-for-python-sdk: staging: ${pipeline.staging}`)
+  await pipeline.build()
+  await pipeline.deployStaging()
+  await pipeline.injectPkgConfig()
+  const products: string[] = []
+  for (const target of cli.targets) products.push(await pipeline.pack(target))
+  pipeline.printProducts(products)
+  await pipeline.syncToPythonRuntime(products)
+}
+
+await main()