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:
@@ -8,14 +8,16 @@ name: Build single-exe
|
||||
# the dsh-jsonrpc-agent-pkg closure manifest with @yao-pkg/pkg into
|
||||
# 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
|
||||
# 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.
|
||||
# - deepseek-harness-python-<target> — the whole python/ directory 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.
|
||||
#
|
||||
# Two explicit triggers, deliberately no per-commit CI: the exe is a
|
||||
# release-style deliverable, and the build (full pnpm build + pnpm deploy +
|
||||
@@ -154,10 +156,24 @@ jobs:
|
||||
- name: Build single-exe
|
||||
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
|
||||
with:
|
||||
name: dsh-jsonrpc-agent-pkg-${{ matrix.target }}
|
||||
path: dist-exe/
|
||||
path: ${{ steps.pack-exe.outputs.tar }}
|
||||
if-no-files-found: error
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
# directory so unpacking never scatters files.
|
||||
#
|
||||
# Excluded: runtime/node/ (dev-only node-mode carrier, ~140MB) plus
|
||||
# __pycache__ / .pytest_cache / .venv / node_modules anywhere (install
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 5543b0ba00816c230dbe4e5bba11b7bf4b38096d
|
||||
README.zh.md: 163b6d8e8373f0f770a5d95be688785907108b8a
|
||||
README.md: 4cf48c55f40ec1c8632582546dcbfb990bbaa660
|
||||
README.zh.md: 0c82569be45b5cefa2ca025728945be1bdefc541
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Python SDK 的运行时载体包(dist 名 `deepseek-harness-runtime-bin`,模
|
||||
|
||||
两种载体承载相同的内容,且只定义一次:本包根目录的 [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
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ _ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm
|
||||
_EXE_ACQUISITION_HINT = (
|
||||
"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` "
|
||||
"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 "
|
||||
f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))."
|
||||
)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 67dbd04520ac01318c1f21393c12b7bf4965b4cb
|
||||
README.zh.md: 36ea4df4ff73e12ce3fda5bbee4108584dd5ad92
|
||||
README.md: ef9f8703f3bdb7b6d2dca28639f3b794083f7905
|
||||
README.zh.md: 31b6e87d7b75cc10cb799862fe6eb0f1f26dccd6
|
||||
@@ -24,4 +24,4 @@ with DeepSeekHarness(
|
||||
`assistant/message` event in the turn. Use `TurnResult.events` for the complete
|
||||
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.
|
||||
@@ -18,4 +18,4 @@ with DeepSeekHarness(
|
||||
|
||||
`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)。
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -60,8 +59,6 @@ class DeepSeekHarness:
|
||||
env["DSH_SESSION_ROOT"] = self.config.session_root
|
||||
if self.config.cordis is not None:
|
||||
env["DSH_CORDIS_CONFIG"] = self.config.cordis
|
||||
else:
|
||||
self._inject_bundled_default_config(env)
|
||||
env["DSH_CWD"] = cwd
|
||||
if self.config.base_url is not None:
|
||||
env["DEEPSEEK_BASE_URL"] = self.config.base_url
|
||||
@@ -109,27 +106,6 @@ class DeepSeekHarness:
|
||||
self._client.close()
|
||||
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":
|
||||
self.start()
|
||||
return Session(self, session_id or f"session-{uuid.uuid4().hex}")
|
||||
|
||||
@@ -67,6 +67,7 @@ class HarnessClient:
|
||||
env = os.environ.copy()
|
||||
if self.config.env:
|
||||
env.update(self.config.env)
|
||||
self._inject_bundled_default_config(env)
|
||||
self._proc = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
@@ -411,6 +412,32 @@ class HarnessClient:
|
||||
) from exc
|
||||
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:
|
||||
with self._lock:
|
||||
self._notification_subscribers.pop(subscription_id, None)
|
||||
|
||||
@@ -17,7 +17,7 @@ import pytest
|
||||
|
||||
from deepseek_harness import DeepSeekHarness, HarnessClient, HarnessConfig
|
||||
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")
|
||||
|
||||
@@ -99,12 +99,16 @@ def test_bundled_runtime_surfaces_unbundled_plugin_failure(tmp_path: Path, mode:
|
||||
|
||||
|
||||
@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(
|
||||
tmp_path: Path, mode: str, monkeypatch: pytest.MonkeyPatch
|
||||
tmp_path: Path, mode: str, ambient_config: str | None, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_launch_args(mode) # skip early when this carrier is unavailable
|
||||
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(
|
||||
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",
|
||||
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:
|
||||
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
|
||||
@@ -548,13 +548,23 @@ with open(os.environ["SEEN"], "w") as seen:
|
||||
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.write_text(
|
||||
"""#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
json.dump({"DSH_CORDIS_CONFIG": os.environ.get("DSH_CORDIS_CONFIG")}, open(os.environ["ENV_DUMP"], "w"))
|
||||
for line in sys.stdin:
|
||||
msg = json.loads(line)
|
||||
if msg.get("method") == "initialize":
|
||||
@@ -566,22 +576,56 @@ for line in sys.stdin:
|
||||
)
|
||||
runtime.chmod(0o755)
|
||||
|
||||
default_config = tmp_path / "default-cordis.yml"
|
||||
module_dir = tmp_path / "deepseek_harness_runtime"
|
||||
module_dir.mkdir()
|
||||
(module_dir / "__init__.py").write_text(
|
||||
f"""
|
||||
def resolve_bundled_launch_args(mode=None):
|
||||
return ({str(runtime)!r},)
|
||||
|
||||
|
||||
def bundled_default_config_path():
|
||||
return {str(default_config)!r}
|
||||
""".strip()
|
||||
)
|
||||
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
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")
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user