review: client-owned default-config injection; tar the bare exe artifact

Address the three ds-review-bot warnings on #253:

- An empty DSH_CORDIS_CONFIG now counts as absent when deciding whether
  to inject the bundled default config, matching the runtime bin's
  config-discovery semantics.
- The injection moves from DeepSeekHarness into HarnessClient.start(),
  so the low-level client's default bundled launch also boots without
  callers duplicating the env setup.
- The bare single-file exe artifact ships inside a tar.gz like the
  Python bundle: upload-artifact's zip transport drops the executable
  bit.
This commit is contained in:
imccyu
2026-07-11 22:54:42 +08:00
parent 81f6aeca3b
commit 8fb70c7d46
12 changed files with 118 additions and 51 deletions

View File

@@ -8,14 +8,16 @@ name: Build single-exe
# the dsh-jsonrpc-agent-pkg closure manifest with @yao-pkg/pkg into # the dsh-jsonrpc-agent-pkg closure manifest with @yao-pkg/pkg into
# dist-exe/. # dist-exe/.
# #
# Each build leg uploads two artifacts: # Each build leg uploads two artifacts (both tar.gz — tar keeps file modes,
# so the exe's executable bit survives; upload-artifact's zip transport
# does not preserve them):
# - dsh-jsonrpc-agent-pkg-<target> — the bare single-file exe, for # - dsh-jsonrpc-agent-pkg-<target> — the bare single-file exe, for
# consumers that want just the binary. # consumers that want just the binary.
# - deepseek-harness-python-<target> — the whole python/ directory as a # - deepseek-harness-python-<target> — the whole python/ directory with
# tar.gz with that exe already embedded (the build script syncs it into # that exe already embedded (the build script syncs it into the Python
# the Python runtime package): unpack and both packages pip install # runtime package): unpack and both packages pip install as-is, the
# as-is, the checked-in default runtime/cordis.yml is editable in # checked-in default runtime/cordis.yml is editable in place, and the
# place, and the embedded exe also runs directly. # embedded exe also runs directly.
# #
# Two explicit triggers, deliberately no per-commit CI: the exe is a # Two explicit triggers, deliberately no per-commit CI: the exe is a
# release-style deliverable, and the build (full pnpm build + pnpm deploy + # release-style deliverable, and the build (full pnpm build + pnpm deploy +
@@ -154,10 +156,24 @@ jobs:
- name: Build single-exe - name: Build single-exe
run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }} run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
# The bare exe ships inside a tar.gz (the mode-preservation note in
# the header): uploading dist-exe/ bare would hand consumers a 0644
# file that subprocess.Popen refuses to run.
- name: Pack bare exe
id: pack-exe
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
exe_tar="dsh-jsonrpc-agent-pkg-${TARGET}.tar.gz"
tar -czf "$exe_tar" -C dist-exe .
ls -lh "$exe_tar"
echo "tar=$exe_tar" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v6
with: with:
name: dsh-jsonrpc-agent-pkg-${{ matrix.target }} name: dsh-jsonrpc-agent-pkg-${{ matrix.target }}
path: dist-exe/ path: ${{ steps.pack-exe.outputs.tar }}
if-no-files-found: error if-no-files-found: error
# After the build step, python/ is already in its complete # After the build step, python/ is already in its complete
@@ -165,9 +181,7 @@ jobs:
# python/sdk-runtime/src/deepseek_harness_runtime/runtime/ next to the # python/sdk-runtime/src/deepseek_harness_runtime/runtime/ next to the
# checked-in default cordis.yml — so packing is all that is left. # checked-in default cordis.yml — so packing is all that is left.
# Everything goes under one top-level deepseek-harness-python/ # Everything goes under one top-level deepseek-harness-python/
# directory so unpacking never scatters files. Shipping a tar (rather # directory so unpacking never scatters files.
# 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 # Excluded: runtime/node/ (dev-only node-mode carrier, ~140MB) plus
# __pycache__ / .pytest_cache / .venv / node_modules anywhere (install # __pycache__ / .pytest_cache / .venv / node_modules anywhere (install

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write
README.md: 5543b0ba00816c230dbe4e5bba11b7bf4b38096d README.md: 4cf48c55f40ec1c8632582546dcbfb990bbaa660
README.zh.md: 163b6d8e8373f0f770a5d95be688785907108b8a README.zh.md: 0c82569be45b5cefa2ca025728945be1bdefc541

View File

@@ -13,7 +13,7 @@ Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injecte
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. 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.
Missing carriers raise `FileNotFoundError` naming the acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` CI workflow and place it in the runtime directory. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers. Missing carriers raise `FileNotFoundError` naming the acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` CI workflow (a tar.gz — tar preserves the executable bit) and unpack it into the runtime directory. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers.
## Resolution API ## Resolution API

View File

@@ -13,7 +13,7 @@ Python SDK 的运行时载体包(dist 名 `deepseek-harness-runtime-bin`,模
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的 deploy root——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。 两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的 deploy root——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或下载 `build-exe-for-python-sdk` CI 工作流的对应平台产物并放入 runtime 目录。获取策略与查找接口刻意分离,之后可以换成按需下载而不动任何调用方。 载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或下载 `build-exe-for-python-sdk` CI 工作流的对应平台产物(tar.gz——tar 保留可执行位)并解包到 runtime 目录。获取策略与查找接口刻意分离,之后可以换成按需下载而不动任何调用方。
## 解析 API ## 解析 API

View File

@@ -36,7 +36,8 @@ _ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm
_EXE_ACQUISITION_HINT = ( _EXE_ACQUISITION_HINT = (
"Two ways to get the executable: run `scripts/build-exe-for-python-sdk.ts` (via tsx) in a " "Two ways to get the executable: run `scripts/build-exe-for-python-sdk.ts` (via tsx) in a "
"deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` " "deepseek-harness checkout, or download the platform artifact of the `build-exe-for-python-sdk` "
"CI workflow; place the file in this package's runtime/ directory. For local development " "CI workflow (a tar.gz — tar preserves the executable bit) and unpack it into this package's "
"runtime/ directory. For local development "
"against a repo source build, explicitly select the dev-only node carrier with " "against a repo source build, explicitly select the dev-only node carrier with "
f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))." f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))."
) )

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write # pnpm run verify-translation-pairing --write
README.md: 67dbd04520ac01318c1f21393c12b7bf4965b4cb README.md: ef9f8703f3bdb7b6d2dca28639f3b794083f7905
README.zh.md: 36ea4df4ff73e12ce3fda5bbee4108584dd5ad92 README.zh.md: 31b6e87d7b75cc10cb799862fe6eb0f1f26dccd6

View File

@@ -24,4 +24,4 @@ with DeepSeekHarness(
`assistant/message` event in the turn. Use `TurnResult.events` for the complete `assistant/message` event in the turn. Use `TurnResult.events` for the complete
event stream, including intermediate assistant messages and tool activity. event stream, including intermediate assistant messages and tool activity.
The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. If neither `cordis` nor `DSH_CORDIS_CONFIG` is set and the launch resolves to the bundled runtime, the bundled default configuration is used; an explicit `runtime_bin` or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](../sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them. The same behavior can be selected for the runtime subprocess with `DSH_CORDIS_CONFIG`. The injection lives in `HarnessClient.start()`, so the low-level client's default launch gets it too: when the launch resolves to the bundled runtime and neither `cordis` nor a non-empty `DSH_CORDIS_CONFIG` is set (the runtime treats an empty value as absent, and so does the injection check), the bundled default configuration is used; an explicit `runtime_bin` or `launch_args_override` disables the injection entirely. See the [sdk-runtime README](../sdk-runtime/README.md) for the runtime carriers (production exe vs dev-only node closure) and how to obtain them.

View File

@@ -18,4 +18,4 @@ with DeepSeekHarness(
`TurnResult.final_response` 是本轮次最后一个 `assistant/message` 事件的文本内容。完整的事件流(包括中间的助手消息与工具活动)用 `TurnResult.events` 获取。 `TurnResult.final_response` 是本轮次最后一个 `assistant/message` 事件的文本内容。完整的事件流(包括中间的助手消息与工具活动)用 `TurnResult.events` 获取。
同样的行为也可以用 `DSH_CORDIS_CONFIG` 为运行时子进程选定。若 `cordis` 与 `DSH_CORDIS_CONFIG` 均未设置,且启动解析到内置运行时,则使用内置的默认配置;显式给出 `runtime_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 node 闭包)及其获取方式见 [sdk-runtime README](../sdk-runtime/README.md)。 同样的行为也可以用 `DSH_CORDIS_CONFIG` 为运行时子进程选定。注入逻辑位于 `HarnessClient.start()`,因此低层客户端的默认启动同样享有它:当启动解析到内置运行时,且 `cordis` 与非空的 `DSH_CORDIS_CONFIG` 均未设置时(运行时把空值当作缺省,注入检查与之一致),使用内置的默认配置;显式给出 `runtime_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 node 闭包)及其获取方式见 [sdk-runtime README](../sdk-runtime/README.md)。

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import os
import uuid import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@@ -60,8 +59,6 @@ class DeepSeekHarness:
env["DSH_SESSION_ROOT"] = self.config.session_root env["DSH_SESSION_ROOT"] = self.config.session_root
if self.config.cordis is not None: if self.config.cordis is not None:
env["DSH_CORDIS_CONFIG"] = self.config.cordis env["DSH_CORDIS_CONFIG"] = self.config.cordis
else:
self._inject_bundled_default_config(env)
env["DSH_CWD"] = cwd env["DSH_CWD"] = cwd
if self.config.base_url is not None: if self.config.base_url is not None:
env["DEEPSEEK_BASE_URL"] = self.config.base_url env["DEEPSEEK_BASE_URL"] = self.config.base_url
@@ -109,27 +106,6 @@ class DeepSeekHarness:
self._client.close() self._client.close()
self._initialized = False self._initialized = False
def _inject_bundled_default_config(self, env: dict[str, str]) -> None:
"""Restore the zero-config experience over the config-mandatory bundled runtime.
The bundled runtime (single-file exe or the dev-only node closure)
always demands an explicit config. When the caller neither provided
``cordis`` nor selected a runtime explicitly (``runtime_bin`` /
``launch_args_override``), and no ambient ``DSH_CORDIS_CONFIG`` exists,
inject the runtime package's checked-in default cordis.yml. With an
explicit runtime or config channel the SDK stays out of the way.
"""
uses_bundled_runtime = self.config.runtime_bin is None and self.config.launch_args_override is None
if not uses_bundled_runtime or "DSH_CORDIS_CONFIG" in env or "DSH_CORDIS_CONFIG" in os.environ:
return
try:
from deepseek_harness_runtime import bundled_default_config_path
except ImportError:
# Only the runtime package's absence reaches here; swallow it so
# HarnessClient.start() reports the actionable install error.
return
env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path())
def start_session(self, session_id: str | None = None) -> "Session": def start_session(self, session_id: str | None = None) -> "Session":
self.start() self.start()
return Session(self, session_id or f"session-{uuid.uuid4().hex}") return Session(self, session_id or f"session-{uuid.uuid4().hex}")

View File

@@ -67,6 +67,7 @@ class HarnessClient:
env = os.environ.copy() env = os.environ.copy()
if self.config.env: if self.config.env:
env.update(self.config.env) env.update(self.config.env)
self._inject_bundled_default_config(env)
self._proc = subprocess.Popen( self._proc = subprocess.Popen(
args, args,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@@ -411,6 +412,32 @@ class HarnessClient:
) from exc ) from exc
return resolve_bundled_launch_args() return resolve_bundled_launch_args()
def _inject_bundled_default_config(self, env: dict[str, str]) -> None:
"""Restore the zero-config experience over the config-mandatory bundled runtime.
The bundled runtime (single-file exe or the dev-only node closure)
always demands an explicit config. When the launch resolves to the
bundled runtime (no ``runtime_bin`` / ``bridge_bin`` /
``launch_args_override``) and the merged subprocess environment has no
non-empty ``DSH_CORDIS_CONFIG`` — the runtime bin treats an empty
value as absent, so this does too — inject the runtime package's
checked-in default cordis.yml. With an explicit runtime or config
channel the client stays out of the way.
"""
uses_bundled_runtime = (
self.config.launch_args_override is None
and self.config.runtime_bin is None
and self.config.bridge_bin is None
)
if not uses_bundled_runtime or env.get("DSH_CORDIS_CONFIG"):
return
# Cannot fail: _default_launch_args() already imported the runtime
# package on this (bundled) path, raising the actionable install
# error when it is absent.
from deepseek_harness_runtime import bundled_default_config_path
env["DSH_CORDIS_CONFIG"] = str(bundled_default_config_path())
def _unsubscribe_notifications(self, subscription_id: str) -> None: def _unsubscribe_notifications(self, subscription_id: str) -> None:
with self._lock: with self._lock:
self._notification_subscribers.pop(subscription_id, None) self._notification_subscribers.pop(subscription_id, None)

View File

@@ -17,7 +17,7 @@ import pytest
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
from deepseek_harness.errors import TransportClosedError from deepseek_harness.errors import TransportClosedError
from deepseek_harness_runtime import bundled_default_config_path, resolve_bundled_launch_args from deepseek_harness_runtime import resolve_bundled_launch_args
_MODES = ("exe", "node") _MODES = ("exe", "node")
@@ -99,12 +99,16 @@ def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode:
@pytest.mark.parametrize("mode", _MODES) @pytest.mark.parametrize("mode", _MODES)
@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
def test_zero_config_run_injects_bundled_default_cordis_config( def test_zero_config_run_injects_bundled_default_cordis_config(
tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch tmp_path: Path, mode: str, ambient_config: str | None, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
_launch_args(mode) # skip early when this carrier is unavailable _launch_args(mode) # skip early when this carrier is unavailable
monkeypatch.setenv("DSH_RUNTIME_MODE", mode) monkeypatch.setenv("DSH_RUNTIME_MODE", mode)
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False) if ambient_config is None:
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
else:
monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
harness = DeepSeekHarness( harness = DeepSeekHarness(
model="deepseek-v4-pro", model="deepseek-v4-pro",
@@ -114,7 +118,8 @@ def test_zero_config_run_injects_bundled_default_cordis_config(
base_url="http://127.0.0.1:9", base_url="http://127.0.0.1:9",
request_timeout_seconds=120, request_timeout_seconds=120,
) )
assert harness.client.config.env is not None
assert harness.client.config.env["DSH_CORDIS_CONFIG"] == str(bundled_default_config_path())
with harness: with harness:
pass # __enter__ boots the runtime on the injected default config; __exit__ shuts it down # __enter__ boots the runtime, which exits with a usage error unless
# HarnessClient.start() injected the bundled default config over the
# unset/empty DSH_CORDIS_CONFIG; __exit__ shuts it down.
pass

View File

@@ -548,13 +548,23 @@ with open(os.environ["SEEN"], "w") as seen:
json.loads(line) json.loads(line)
def test_client_uses_bundled_runtime_package_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def _install_fake_bundled_runtime(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> Path:
"""Fake the deepseek-harness-runtime-bin package on sys.path.
A stub exe that dumps DSH_CORDIS_CONFIG to $ENV_DUMP before serving
initialize/shutdown, plus a module exposing the resolution surface the
client consumes. Returns the fake bundled default config path.
"""
runtime = tmp_path / "dsh-jsonrpc-agent" runtime = tmp_path / "dsh-jsonrpc-agent"
runtime.write_text( runtime.write_text(
"""#!/usr/bin/env python3 """#!/usr/bin/env python3
import json import json
import os
import sys import sys
json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
for line in sys.stdin: for line in sys.stdin:
msg = json.loads(line) msg = json.loads(line)
if msg.get("method") == "initialize": if msg.get("method") == "initialize":
@@ -566,22 +576,56 @@ for line in sys.stdin:
) )
runtime.chmod(0o755) runtime.chmod(0o755)
default_config = tmp_path / "default-cordis.yml"
module_dir = tmp_path / "deepseek_harness_runtime" module_dir = tmp_path / "deepseek_harness_runtime"
module_dir.mkdir() module_dir.mkdir()
(module_dir / "__init__.py").write_text( (module_dir / "__init__.py").write_text(
f""" f"""
def resolve_bundled_launch_args(mode=None): def resolve_bundled_launch_args(mode=None):
return ({str(runtime)!r},) return ({str(runtime)!r},)
def bundled_default_config_path():
return {str(default_config)!r}
""".strip() """.strip()
) )
monkeypatch.syspath_prepend(str(tmp_path)) monkeypatch.syspath_prepend(str(tmp_path))
monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False) monkeypatch.delitem(sys.modules, "deepseek_harness_runtime", raising=False)
return default_config
with HarnessClient() as client:
@pytest.mark.parametrize("ambient_config", [None, ""], ids=["unset", "empty-counts-as-absent"])
def test_client_default_launch_uses_bundled_runtime_and_injects_default_config(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ambient_config: str | None
) -> None:
env_dump = tmp_path / "env.json"
default_config = _install_fake_bundled_runtime(tmp_path, monkeypatch)
if ambient_config is None:
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
else:
monkeypatch.setenv("DSH_CORDIS_CONFIG", ambient_config)
with HarnessClient(HarnessConfig(env={"ENV_DUMP": str(env_dump)})) as client:
init = client.initialize(cwd="/workspace", model="deepseek-v4-pro") init = client.initialize(cwd="/workspace", model="deepseek-v4-pro")
assert init.serverInfo.name == "bundled-runtime" assert init.serverInfo.name == "bundled-runtime"
assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == str(default_config)
def test_client_respects_explicit_config_over_bundled_default(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
env_dump = tmp_path / "env.json"
_install_fake_bundled_runtime(tmp_path, monkeypatch)
monkeypatch.delenv("DSH_CORDIS_CONFIG", raising=False)
with HarnessClient(
HarnessConfig(env={"ENV_DUMP": str(env_dump), "DSH_CORDIS_CONFIG": "./explicit.yml"})
) as client:
client.initialize(cwd="/workspace", model="deepseek-v4-pro")
assert json.loads(env_dump.read_text())["DSH_CORDIS_CONFIG"] == "./explicit.yml"
def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None: def test_client_reports_missing_bundled_runtime_dependency(monkeypatch: pytest.MonkeyPatch) -> None: