diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index d33db48d65..cba3cd1c5c 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 04596c82f75cc68e67d599c11b76fe68f5a3be90 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 7fe965b736f1747fe56da3591bea4a5047a9fcad +2026-07-10-single-file-executable-sdk-runtime-distribution.md: ab09691b48684b8ebae269df4d585b499423e671 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d986105b087357bd8301251dca14736a5b427bbb diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 04596c82f7..ab09691b48 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -66,7 +66,7 @@ The Python client launches the packaged `dsh` command with the selected profile ## Testing -The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. +The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The spawn-node scenario drives the platform shell tool through a command starting with `node` and requires the machine's own Node version in the tool result with no `PKG_EXECPATH` in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 7fe965b736..d986105b08 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -66,7 +66,7 @@ exe 内支持 `dsh-workflow-worker-thread` 与 `dsh-code-runtime-worker-thread` ## 测试 -验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都会把两个 wheel 包安装进 checkout 外的干净 venv,证明版本相同以及已安装模块/可执行文件的位置,再通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。文件系统搜索场景要求模型通过目标平台的 `-rg` 伴随文件调用 `glob` 与 `grep`。MCP 场景会启动临时外部 stdio server,刻意延迟首次 `tools/list` 响应,随后立即启动第一个 SDK 提示词;该提示词必须看到并调用已发现的工具,从而证明 `initialize` 是真正以 Loader 插件树完全稳定为准的就绪边界,而不是依赖定时 sleep。同一项安装后运行还会经 Python SDK 比较一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话 JSONL 日志中不透明的消息、agent、工作流运行与会话 ID。可信拉取请求会增加真实提供方双轮文件写入/读取,并要求外部字节、工具调用、已完成原因与持久化日志一致。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。 +验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节(VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行)。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都会把两个 wheel 包安装进 checkout 外的干净 venv,证明版本相同以及已安装模块/可执行文件的位置,再通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。文件系统搜索场景要求模型通过目标平台的 `-rg` 伴随文件调用 `glob` 与 `grep`。spawn-node 场景驱动平台 shell 工具执行以 `node` 开头的命令,要求工具结果给出机器自身的 Node 版本且子进程环境中无 `PKG_EXECPATH`,把打包运行时钉死在「pkg 升级重录 child_process 补丁也不得回归」的行为上。MCP 场景会启动临时外部 stdio server,刻意延迟首次 `tools/list` 响应,随后立即启动第一个 SDK 提示词;该提示词必须看到并调用已发现的工具,从而证明 `initialize` 是真正以 Loader 插件树完全稳定为准的就绪边界,而不是依赖定时 sleep。同一项安装后运行还会经 Python SDK 比较一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture(测试前置数据)会显式禁用组合包中未使用的 Bash 和本地 skill(技能)发现,使其工具集不依赖仓库外部状态;比较时会规范化 SDK 结果与通知流,以及父会话和两个子会话 JSONL 日志中不透明的消息、agent、工作流运行与会话 ID。可信拉取请求会增加真实提供方双轮文件写入/读取,并要求外部字节、工具调用、已完成原因与持久化日志一致。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。 手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。 diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 2e02a8527c..a0ab195b2e 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -10,6 +10,7 @@ import importlib.metadata import json import os import queue +import shutil import subprocess import sys import sysconfig @@ -55,6 +56,17 @@ MINIMAL_SHELL_COMMAND = ( ) ) MINIMAL_SHELL_SECOND_CWD = str(Path(tempfile.gettempdir()).resolve()) if IS_WINDOWS else "/tmp" +SPAWN_NODE_PROMPT = "Run node --version through the packaged shell tool." +SPAWN_NODE_TEXT = "spawn node smoke ok" +SPAWN_NODE_CALL_ID = "spawn-node-shell" +# The POSIX command string starts with `node ` inside the shell tool's `bash -c` +# argv, the exact form @yao-pkg/pkg's unpatched SEA bootstrap rewrites to the +# executable itself while stamping PKG_EXECPATH into the child environment. +SPAWN_NODE_COMMAND = ( + 'node --version; if ($env:PKG_EXECPATH) { "PKG_EXECPATH=$env:PKG_EXECPATH" } else { "PKG_EXECPATH=ABSENT" }' + if IS_WINDOWS + else 'node --version; echo "PKG_EXECPATH=${PKG_EXECPATH:-ABSENT}"' +) LEGACY_CUSTOM_DISABLED_ROWS = ( "agent-instructions", "goal", @@ -309,6 +321,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: fs_search = fs_search_tool_followup(call_id, tool_name, tool_text) if fs_search is not None: return fs_search + spawn_node = spawn_node_tool_followup(call_id, tool_name, tool_text) + if spawn_node is not None: + return spawn_node minimal = minimal_tool_followup(body, call_id, tool_name, tool_text) if minimal is not None: return minimal @@ -351,6 +366,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: CODE_PROMPT, WORKFLOW_PROMPT, FS_SEARCH_PROMPT, + SPAWN_NODE_PROMPT, MCP_PROMPT, RESTART_FIRST_PROMPT, RESTART_SECOND_PROMPT, @@ -414,6 +430,13 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: "grep", {"pattern": FS_SEARCH_MARKER, "path": "."}, ) + if prompt == SPAWN_NODE_PROMPT: + assert_advertised_tool(body, MINIMAL_SHELL_TOOL) + return tool_call_chunks( + SPAWN_NODE_CALL_ID, + MINIMAL_SHELL_TOOL, + {"command": SPAWN_NODE_COMMAND, "description": "Report the reachable Node version"}, + ) if prompt == MCP_PROMPT: assert_advertised_tool(body, "mcp__fixture__add") return tool_call_chunks( @@ -469,6 +492,36 @@ def fs_search_tool_followup( raise AssertionError(f"unexpected filesystem-search follow-up: {call_id} {tool_name}: {tool_text}") +def host_node_version() -> str: + """The machine's own `node --version` line, the required shell resolution target.""" + node = shutil.which("node") + if node is None: + raise AssertionError("the spawn-node scenario requires Node on PATH for comparison") + return subprocess.run( + [node, "--version"], capture_output=True, text=True, check=True, + ).stdout.strip() + + +def spawn_node_tool_followup( + call_id: str, + tool_name: str, + tool_text: str, +) -> list[dict[str, object]] | None: + """Verify the packaged shell reached the machine's Node with a clean environment.""" + if call_id != SPAWN_NODE_CALL_ID: + return None + if tool_name != MINIMAL_SHELL_TOOL: + raise AssertionError(f"spawn-node follow-up used an unexpected tool: {tool_name}") + expected = host_node_version() + if expected not in tool_text: + raise AssertionError( + f"packaged shell did not reach the machine's node {expected}: {tool_text}" + ) + if "PKG_EXECPATH=ABSENT" not in tool_text: + raise AssertionError(f"PKG_EXECPATH reached the shell child environment: {tool_text}") + return text_chunks(SPAWN_NODE_TEXT) + + def minimal_tool_followup( body: dict[str, object], call_id: str, @@ -711,7 +764,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--scenario", - choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "direct"), + choices=("all", "sdk-default", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-mcp", "sdk-snapshot", "sdk-restart", "sdk-profile-plugin", "sdk-live", "direct"), default="all", ) parser.add_argument("--exe", type=Path) @@ -730,7 +783,7 @@ def main() -> None: parser.error("--scenario sdk-profile-plugin requires --installed-wheel") if args.installed_wheel: args.exe = assert_installed_wheel_environment() - if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-snapshot", "sdk-restart", "direct"} and args.exe is None: + if args.scenario in {"all", "sdk-custom", "sdk-minimal", "sdk-fs-search", "sdk-spawn-node", "sdk-snapshot", "sdk-restart", "direct"} and args.exe is None: parser.error("--exe is required for custom, minimal, snapshot, and direct scenarios") if args.update_snapshots and args.scenario not in {"all", "sdk-minimal", "sdk-snapshot", "sdk-restart"}: parser.error("--update-snapshots requires --scenario sdk-minimal, sdk-snapshot, sdk-restart, or all") @@ -754,6 +807,9 @@ def main() -> None: if args.scenario in {"all", "sdk-fs-search"}: assert args.exe is not None smoke_sdk_fs_search(model.url, args.exe.resolve()) + if args.scenario in {"all", "sdk-spawn-node"}: + assert args.exe is not None + smoke_sdk_spawn_node(model.url, args.exe.resolve()) if args.scenario in {"all", "sdk-mcp"}: smoke_sdk_mcp(model.url, None if args.exe is None else args.exe.resolve()) if args.scenario in {"all", "sdk-snapshot"}: @@ -1049,6 +1105,36 @@ def smoke_sdk_fs_search(base_url: str, executable: Path) -> None: assert_session_log(sessions, root, FS_SEARCH_TEXT, FS_SEARCH_MARKER, "needle.txt") +def smoke_sdk_spawn_node(base_url: str, executable: Path) -> None: + """A shell command starting with `node` must reach the machine's Node, not the executable.""" + from deepseek_harness import DeepSeekHarness + + with tempfile.TemporaryDirectory(prefix="dsh-sdk-spawn-node-") as temporary: + root = Path(temporary).resolve() + dsh_home = root / "home" + sessions = dsh_home / "sessions" + patch = write_profile_patch(root, "spawn-node.patch.yml", sessions, []) + with DeepSeekHarness( + provider="deepseek-official", + model="smoke-model", + cwd=str(root), + dsh_bin=str(executable), + dsh_home=str(dsh_home), + patches=(str(patch),), + env={ + "DSH_PERMISSION_MODE": "danger-full-access", + "DSH_TELEMETRY_DISABLED": "1", + }, + api_key="sk-keyless-smoke", + base_url=base_url, + request_timeout_seconds=60, + ) as harness: + result = harness.run(SPAWN_NODE_PROMPT, session_id="spawn-node-smoke") + + assert result.final_response == SPAWN_NODE_TEXT, result.final_response + assert_session_log(sessions, root, SPAWN_NODE_TEXT, "PKG_EXECPATH=ABSENT") + + def smoke_sdk_mcp(base_url: str, executable: Path | None) -> None: """Discover and call an external stdio MCP tool through the packaged client.""" from deepseek_harness import DeepSeekHarness