mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #2958 from deepseek-harness/worktree/python-sdk-dsh-cli
feat(python): launch the SDK through packaged dsh profiles
This commit is contained in:
+2
-2
@@ -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: e5438fc01fdd634cd4d8535dfa37a1de356da04d
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 218133f077bed7584b6a26fea5f939b64d4fd670
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.md: e46dbbc119e2078e44632d81b333c8be5ab9d6d7
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a1e3f445ea1c1f03df2b349a4391534a5502c3
|
||||
|
||||
+9
-9
@@ -23,40 +23,40 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h
|
||||
|
||||
Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
|
||||
|
||||
### The serving interface is a plugin: the two packages sdk/server + sdk/python-runtime
|
||||
### The serving interface is a plugin inside the dsh application
|
||||
|
||||
The deterministic protocol implementation (`server.ts` / `transport.ts`) lands as two packages on the existing `acp/acp` + `examples/acp-demo` pattern — the serving surface is itself a plugin:
|
||||
The deterministic serving surface is a plugin selected by the packaged `dsh` application:
|
||||
|
||||
- [`packages/sdk/server`](../../../../packages/sdk/server/README.md) (`@deepseek-ai/dsh-sdk-jsonrpc-server`): the pure protocol plugin; on apply it mounts `HarnessSdkJsonRpcServer` plus a line-delimited JSON-RPC transport on the process stdio, with disposal through `ctx.effect()`. Whether to serve is decided by `cordis.yml`; a yml that does not mount it is a legitimate process that does not serve. Protocol-level exit belongs to the plugin (after answering and flushing the `shutdown` response it disposes the root runtime so persistence drains, then `exit(0)`; an HMR-style unload only stops the service without exiting the process).
|
||||
- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.md) (`@deepseek-ai/dsh-sdk-python-runtime`): a private packaged entry — `installFailLoud` + `loadEnv` + config discovery + `boot()` from [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts), done once boot completes; the server is brought up by the `dsh-sdk-jsonrpc-server` entry in the yml. Its only dependency is app-boot. Process-level exit belongs to the packaged entry (stdin EOF/SIGTERM → dispose then 0, SIGINT → 130).
|
||||
- [`apps/cli`](../../../../apps/cli/README.md) (`@deepseek-ai/dsh`): the packaged application entry; its `sdk` profile mounts `dsh-sdk-jsonrpc-server`, and the CLI owns environment layering, profile composition, stdin/signal shutdown, and process exit.
|
||||
|
||||
Config discovery has two channels and fails loudly when both are missing: the `DSH_CORDIS_CONFIG` environment variable first (the SDK client convention), then an argv positional argument; no default path and no built-in fallback whatsoever — "the plugins actually booted are decided by an external cordis.yml" is a hard semantic.
|
||||
The Python client supplies an explicit Harness home and selects the `sdk` profile plus ordered patch files. A missing home, profile, bundle, or server row fails loudly; there is no external complete-config fallback. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this application surface.
|
||||
|
||||
### Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root
|
||||
|
||||
Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
|
||||
|
||||
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-sdk-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
|
||||
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
|
||||
|
||||
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers or extend the bridge to MCP Resources and Prompts. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
|
||||
|
||||
### Build pipeline and artifacts
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-sdk-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore any direct workspace package that legacy deploy hoisted back under the source manifest's `node_modules`, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source; CI rebuilds that addon inside the matching manylinux 2.28 container before packaging, and the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it into the runtime directory. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all three targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` and the `build-exe` label can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS verifies that the executable's deployment target fits the wheel tag. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
|
||||
|
||||
### Python SDK distribution: two carriers, exe for production, node for development
|
||||
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe with its required `-rg` sidecar and optional macOS helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
|
||||
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched `-rg` sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms.
|
||||
|
||||
The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-sdk-jsonrpc-server` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.
|
||||
The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
|
||||
|
||||
### Naming lineage
|
||||
|
||||
`@deepseek-ai/dsh-sdk-python-runtime` (the private carrier) → `dsh-sdk-python-runtime-closure` (the deploy manifest; no scope prefix, so it is not a dsh release package) → `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules remain `deepseek_harness` / `deepseek_harness_runtime`.
|
||||
`dsh-python-runtime-closure` is the private deploy manifest and `deepseek-harness-sdk-runtime-<platform>-<arch>` is the executable family. The wire `serverInfo.name` is `deepseek-harness-sdk-runtime`; the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules are `deepseek_harness` / `deepseek_harness_runtime`.
|
||||
|
||||
## Disposition of worker-style plugins
|
||||
|
||||
|
||||
+9
-9
@@ -23,40 +23,40 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后
|
||||
|
||||
术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的「快照」(ACP(Agent Client Protocol)回放预期输出、`$DSH_SNAPSHOT`)无关,本文用「VFS」指前者。
|
||||
|
||||
### 对外服务接口也是插件:sdk/server + sdk/python-runtime 两个包
|
||||
### 对外服务接口是 dsh 应用中的插件
|
||||
|
||||
确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件:
|
||||
确定性服务接口由打包后的 `dsh` 应用选择为插件:
|
||||
|
||||
- [`packages/sdk/server`](../../../../packages/sdk/server/README.zh.md)(`@deepseek-ai/dsh-sdk-jsonrpc-server`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkJsonRpcServer` 与按行分隔的 JSON-RPC 传输层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR(热模块替换)式卸载只停止服务,不退出进程)。
|
||||
- [`packages/sdk/python-runtime`](../../../../packages/sdk/python-runtime/README.zh.md)(`@deepseek-ai/dsh-sdk-python-runtime`):私有打包入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()`;`boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-sdk-jsonrpc-server` 条目启动。它只依赖 `app-boot`。进程级退出归打包入口所有(stdin EOF/SIGTERM → dispose 后返回 0,SIGINT → 130)。
|
||||
- [`apps/cli`](../../../../apps/cli/README.zh.md)(`@deepseek-ai/dsh`):打包后的应用入口;其 `sdk` profile 挂载 `dsh-sdk-jsonrpc-server`,CLI 负责环境分层、profile 组合、stdin/signal 关闭与进程退出。
|
||||
|
||||
配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。
|
||||
Python 客户端提供显式 Harness home,并选择 `sdk` profile 与有序 patch 文件。缺失 home、profile、bundle 或 server 配置项都会明确失败;不存在外部完整配置回退。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该应用接口。
|
||||
|
||||
### 插件解析:VFS 装载真实包树,闭包 manifest(元数据清单)就是部署根目录
|
||||
|
||||
exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名则从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。
|
||||
|
||||
部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-sdk-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。
|
||||
部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)(`dsh-python-runtime-closure`,pnpm 工作区成员、零代码纯依赖 manifest),也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 读取每个已发布的 `packages/preset/agent-presets/presets/*/agent.cordis.yml`,针对 `python/sdk-runtime/platforms.json` 中的每个目标解析比较 `process.platform` 的 `disabled` 条件,并要求该目标启用的每个工作区插件都通过显式的 `workspace:` 依赖列在运行时根目录。它还遍历该 manifest 覆盖的全部工作区包,要求每个非可选的工作区对等依赖(peer dependency)都显式列出,并报告“preset 或引用包 → 缺失依赖”的完整链路;无法识别的平台条件会保持启用,避免因不支持的表达式遗漏插件。`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。
|
||||
|
||||
部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server,也不将桥接范围扩展到 MCP Resources 和 Prompts。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
|
||||
|
||||
### 构建流水线与产物
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-sdk-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复被 legacy deploy 提升回源 manifest 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`;`assets` 使用全量 glob,因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`;CI 会在打包前进入匹配架构的 manylinux 2.28 容器重新构建该 addon,而 `--legacy` 部署会省略这一副作用目录,因此构建器会把它从根安装目录复制到暂存闭包。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/` 并拷回运行时目录。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。
|
||||
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部三个目标;`workflow_dispatch` 与 `build-exe` 标签仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
|
||||
|
||||
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
|
||||
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**;`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-sdk-python-runtime/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
|
||||
|
||||
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 `-rg` 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
|
||||
|
||||
exe「必须显式配置」的硬语义不变;零配置体验由包装层恢复:调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml`(`agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-sdk-jsonrpc-server` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。
|
||||
Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
|
||||
|
||||
### 命名血统
|
||||
|
||||
`@deepseek-ai/dsh-sdk-python-runtime`(私有载体)→ `dsh-sdk-python-runtime-closure`(部署 manifest;没有作用域前缀,因此不属于 dsh 发布包)→ `dsh-jsonrpc-agent-pkg-<platform>-<arch>`(exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`(协议稳定值);Python 分发包名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`。
|
||||
`dsh-python-runtime-closure` 是私有部署 manifest,`deepseek-harness-sdk-runtime-<platform>-<arch>` 是可执行文件族。协议字段 `serverInfo.name` 是 `deepseek-harness-sdk-runtime`;Python 分发包名是 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名是 `deepseek_harness` / `deepseek_harness_runtime`。
|
||||
|
||||
## 工作线程插件
|
||||
|
||||
|
||||
@@ -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-08-05-profile-plugin-bundles.md
|
||||
2026-08-05-profile-plugin-bundles.md: c9f685eecddcd4ea8d8580becef3c32a9693a329
|
||||
2026-08-05-profile-plugin-bundles.zh.md: 3f2db5ea7653fde356f6ae8f21898f1fdada3926
|
||||
2026-08-05-profile-plugin-bundles.md: 493568691dac3a54185f11cbbf8162bf6b6355b1
|
||||
2026-08-05-profile-plugin-bundles.zh.md: adfa95b6f8d0fdd6fe3c0ebbc7a62d935ebb1987
|
||||
|
||||
@@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
|
||||
|
||||
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](2026-08-06-app-owned-command-line.md).
|
||||
|
||||
The default Profile templates use `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, while headless owns its task positional. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
|
||||
The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
|
||||
|
||||
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
|
||||
|
||||
@@ -27,7 +27,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th
|
||||
|
||||
## Consequences
|
||||
|
||||
- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile; the repository no longer needs a row for every deployment shape.
|
||||
- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile, without a repository row for every deployment shape.
|
||||
- `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone.
|
||||
- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly.
|
||||
- Backends reject nothing old on disk (pre-release stance): `$DSH_HOME/config.yaml` is simply no longer read.
|
||||
- Under the pre-release stance, backends carry no compatibility behavior for old on-disk configuration; `$DSH_HOME/config.yaml` is ignored.
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层与 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](2026-08-06-app-owned-command-line.zh.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。
|
||||
|
||||
默认 Profile 模板使用的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。
|
||||
默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.zh.md)负责 headless 组合约定。
|
||||
|
||||
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
|
||||
|
||||
@@ -27,7 +27,7 @@ Status: implemented
|
||||
|
||||
## Consequences
|
||||
|
||||
- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装;仓库不再需要为每种部署形态各留一行。
|
||||
- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装,无需在仓库中为每种部署形态各留一行。
|
||||
- `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。
|
||||
- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会响亮失败。
|
||||
- 后端不拒绝磁盘上的任何旧格式(发布前姿态):`$DSH_HOME/config.yaml` 只是不再被读取。
|
||||
- 按发布前姿态,后端不携带旧磁盘配置的兼容行为;`$DSH_HOME/config.yaml` 会被忽略。
|
||||
|
||||
+2
-2
@@ -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-08-11-repository-naming-contract-and-rename-ledger.md
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.md: 2de88df5f1a5037d32daa9a8ee9cd1edfc60528a
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 0cbbbee561358614da0b20e49c610fbee1d4e537
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.md: 403a7288bb3af0eff09b229e7d399b94492ae6c0
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 644e1c0bebfc43414e9e67def8d6c42acddf4c72
|
||||
|
||||
+4
-4
@@ -274,11 +274,11 @@ Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. This dec
|
||||
| `E2BSandboxService` | `E2BRuntime` | The class creates, reuses, and disposes the E2B execution environment used by filesystem and subprocess adapters. It is broader than one sandbox handle and narrower than a generic owner. Keep `@deepseek-ai/dsh-e2b`, `ctx.e2b`, and the `e2b/` group. |
|
||||
| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | The package is the Host plugin that serves the frontend assets. The prefix distinguishes it from frontend application code. |
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | The class is a Remote-only adapter from the live Loader tree to the `pluginInventory/list` RPC. It owns no same-process service, cache, history, or mutation path. `Gateway` states the role that exists. |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | The private package carries only the temporarily separate Python SDK runtime; the [single dsh launcher decision](2026-08-22-single-dsh-application-launcher.md) owns its application-boundary change. |
|
||||
| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | The carrier is production packaging infrastructure for the Python SDK, not a demo bundle. |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The direct-config runnable example belongs specifically to the Python SDK exception. |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-python-runtime` | removed | The Python runtime packages the existing `@deepseek-ai/dsh` CLI and its `sdk` profile; a private application package would recreate a second launcher. |
|
||||
| `packages/examples/jsonrpc-demo/`, `packages/sdk/python-runtime/` | removed | The Python runtime wheel's closure manifest owns packaging without a separate application package. |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The example demonstrates Python use of the `sdk` profile and ordered patches. |
|
||||
| `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | The package is the ACP profile's application bundle, not a standalone demo bin. |
|
||||
| Deploy-root manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | The manifest defines the private Python runtime dependency closure. The Python-visible executable basename remains fixed until its documented profile migration. |
|
||||
| Deploy-root manifests `dsh-jsonrpc-agent-pkg`, `dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | The zero-code manifest defines the Python runtime wheel's complete `dsh` dependency closure without naming a separate SDK application. |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. |
|
||||
|
||||
Keep atomic-write, brand, native-command, timeout utility, directory-picker, `dsh-base`, `dsh-web-app`, `dsh-sdk-app`, `dsh-acp-app`, app boot, CLI names, and the `headless` package, bundle, and example identity. `headless` is the intended product essence and may later support more than one-shot execution.
|
||||
|
||||
+4
-4
@@ -274,11 +274,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
|
||||
| `E2BSandboxService` | `E2BRuntime` | 该类创建、复用和释放文件系统与子进程适配器所使用的 E2B 执行环境。它比单个沙箱句柄的职责更广,又比通用所有者更具体。保留 `@deepseek-ai/dsh-e2b`、`ctx.e2b` 和 `e2b/` 组。 |
|
||||
| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | 该包是提供前端资源的 Host 插件。此前缀可将它与前端应用代码区分开。 |
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | 该类只负责把实时 Loader 树适配到 `pluginInventory/list` RPC。它不拥有同进程服务、缓存、历史或修改路径。`Gateway` 准确说明现有角色。 |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-python-runtime` | 该私有包只承载暂时独立的 Python SDK 运行时;其应用边界变更由[单一 dsh 启动器决策](2026-08-22-single-dsh-application-launcher.zh.md)负责。 |
|
||||
| `packages/examples/jsonrpc-demo/` | `packages/sdk/python-runtime/` | 该载体是 Python SDK 的生产打包基础设施,不是演示组合包。 |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 直读配置的可运行示例专属于 Python SDK 例外。 |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-python-runtime` | 已删除 | Python 运行时打包现有 `@deepseek-ai/dsh` CLI 与其 `sdk` profile;私有应用包会重新产生第二个启动器。 |
|
||||
| `packages/examples/jsonrpc-demo/`、`packages/sdk/python-runtime/` | 已删除 | Python 运行时 wheel 的闭包 manifest 负责打包,无需独立应用包。 |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 该示例演示 Python 使用 `sdk` profile 与有序 patch。 |
|
||||
| `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | 该包是 ACP profile 的应用组合包,不是独立 demo bin。 |
|
||||
| 部署根 manifest `dsh-jsonrpc-agent-pkg` | `dsh-sdk-python-runtime-closure` | 该 manifest 定义私有 Python 运行时依赖闭包。面向 Python 的可执行文件基本名称保持不变,直至完成已记录的 profile 迁移。 |
|
||||
| 部署根 manifest `dsh-jsonrpc-agent-pkg`、`dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | 该零代码 manifest 定义 Python 运行时 wheel 的完整 `dsh` 依赖闭包,不再命名独立 SDK 应用。 |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 |
|
||||
|
||||
保留 atomic-write、brand、native-command、timeout 实用工具、目录选择器、`dsh-base`、`dsh-web-app`、`dsh-sdk-app`、`dsh-acp-app`、应用启动、CLI(命令行界面)名称,以及 `headless` 包、组合包和示例身份。`headless` 是预期的产品本质,未来也可以支持不止一次性执行。
|
||||
|
||||
+2
-2
@@ -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-08-22-single-dsh-application-launcher.md
|
||||
2026-08-22-single-dsh-application-launcher.md: 69188e806d9192d230d1f1b52daf27a3b30481be
|
||||
2026-08-22-single-dsh-application-launcher.zh.md: a6bb1909b019ea0d386bd2f4a1bb8f5199ef1974
|
||||
2026-08-22-single-dsh-application-launcher.md: 48a45cb2454b5532a78474203b6d88aef3dd0697
|
||||
2026-08-22-single-dsh-application-launcher.zh.md: dbf2fb3a5bdc16208b0435482d8d0851bd7c44d7
|
||||
|
||||
+15
-18
@@ -8,19 +8,19 @@ English | [中文](2026-08-22-single-dsh-application-launcher.zh.md)
|
||||
|
||||
DeepSeek Harness application processes need one owner for composition, plugin resolution, environment discovery, shutdown, and user customization. A dedicated app bin with a complete `cordis.yml` creates a second lifecycle beside profile launch: plugins installed into a profile do not reach it, behavior drifts from `dsh-base`, and SDK callers learn arbitrary process argv instead of the product's composition model.
|
||||
|
||||
The Python SDK distributes a native executable and three platform wheels whose embedded direct-config runtime cannot change launch architecture without rebuilding and validating the complete VFS closure. That distribution needs an explicit temporary exception, not a second general Node application pattern.
|
||||
The Python SDK distributes a native executable and three platform wheels. Its packaged process must use the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence.
|
||||
|
||||
## Decision
|
||||
|
||||
### Launch scope
|
||||
|
||||
Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry.
|
||||
Every supported Node application starts through the `dsh` CLI and one named profile. The shipped application commands are `dsh web`, `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`; `dsh web` is the deliberate convenience alias for `--profile web`, not another application entry.
|
||||
|
||||
Vendor CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are outside the application-launch inventory. A package app bin or root demo that launches a package entry is not an accepted extension point.
|
||||
|
||||
### Profile applications
|
||||
|
||||
`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior.
|
||||
`@deepseek-ai/dsh-sdk-app` and `@deepseek-ai/dsh-acp-app` compose the full protocol applications over `@deepseek-ai/dsh-base`. The SDK bundle adds the JSON-RPC server plus app-owned help and stdio lifetime; the ACP bundle adds the automation-only ACP server plus the same application responsibilities. Both adopt the base model, tools, persistence, settings, credentials, policy, and environment behavior. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) reuses SDK startup and JSON-RPC serving but deliberately owns a complete explicit tree without `dsh-base`.
|
||||
|
||||
Profile manifests own patch reload:
|
||||
|
||||
@@ -29,11 +29,12 @@ Profile manifests own patch reload:
|
||||
| `web` | `live` |
|
||||
| `headless` | `startup` |
|
||||
| `sdk` | `startup` |
|
||||
| `sdk-minimal` | `startup` |
|
||||
| `acp` | `startup` |
|
||||
|
||||
Custom profiles default to `live`. A startup profile still applies its bundle, profile, home-level, and invocation `--patch` layers, but it does not watch them after boot. `dsh-base` inserts the module-HMR row disabled; a profile with a tested source-module reload lifecycle must enable it explicitly. None of the shipped profiles enable server module HMR: `patchReload: live` uses the launcher's config-only watcher while the startup profiles install no watcher. SDK and ACP cannot safely replace their server, agents, persistence, or tool registry inside one owned stdio connection.
|
||||
|
||||
The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Model-visible tool and persistence defaults come from `dsh-base`, and runnable snapshots own those assembled application outputs.
|
||||
The shipped protocol profiles reserve stdout for protocol frames, expose help without starting transport, and route stdin EOF and signals through bounded root disposal. ACP remains automation-only. The SDK JSON-RPC methods, notification fields, and `initialize.serverInfo.name` remain stable. Full-profile model-visible tool and persistence defaults come from `dsh-base`; `sdk-minimal` owns its explicit defaults. Runnable snapshots own the assembled application outputs.
|
||||
|
||||
### TypeScript SDK customization
|
||||
|
||||
@@ -43,25 +44,21 @@ SDK users customize plugins through profiles. `dsh plugin --profile <name> ...`
|
||||
|
||||
Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inherited `DSH_HOME`, then `~/.dsh`. `subagent-dsh-sdk` instead requires an explicit absolute home, so a nested runtime cannot discover a person's profiles, installed plugins, credentials, or sessions through the operating-system home. DSH-specific ACP child examples also pass an isolated home; the ACP backend itself remains generic for non-DSH agents.
|
||||
|
||||
### Python exception and names
|
||||
### Python runtime
|
||||
|
||||
The Python SDK's direct-config application lives in the private `packages/sdk/python-runtime` package named `@deepseek-ai/dsh-sdk-python-runtime`. Its only packaged executable entry is `lib/packaged-bin.js`, consumed by the private `dsh-sdk-python-runtime-closure` deploy root. It has no public npm bin. The runnable direct Python example is `examples/python-sdk-agent`.
|
||||
The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable Python example selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application.
|
||||
|
||||
Python-observable behavior remains fixed: Python API, SDK wire, default `cordis.yml`, environment variables, wheel distribution names, packaged executable names, sidecar names, explicit runtime options, zero-config behavior, and supported platforms. The stable SDK family remains `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, `@deepseek-ai/dsh-sdk-jsonrpc-server`, and wire identity `deepseek-harness-sdk-runtime`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias.
|
||||
The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias.
|
||||
|
||||
### Enforcement
|
||||
|
||||
`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, test support, and the private Python carrier. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates.
|
||||
|
||||
## Deferred Python migration
|
||||
|
||||
The Python runtime follow-up must move the packaged process through `dsh --profile sdk`, preserve the wheel's closed dependency and native sidecar behavior, and delete `@deepseek-ai/dsh-sdk-python-runtime`. Only after those conditions pass on Linux x64, Linux arm64, and macOS arm64 does the executable family change from `dsh-jsonrpc-agent-pkg-<platform>-<arch>` to `deepseek-harness-sdk-runtime-<platform>-<arch>`. The temporary carrier and current artifact names make that obligation visible without weakening current Python compatibility.
|
||||
`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, and test support. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
This decision supersedes the application-launch and package-name facts in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md), [TypeScript SDK client and subagent backend](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md), [remove the SDK project toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md), and [single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md). Those notes retain independent authority for profile layering, client/wire semantics, deleted project tooling, and native packaging.
|
||||
|
||||
The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. No active note is fully superseded or eligible for archival.
|
||||
The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-only-protocol.md) remains authoritative for ACP wire and interaction scope. The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) remains authoritative for role-based package names. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) partially supersedes this note's base-first rule and complete-tree alternative while retaining this note's launcher ownership. No active note is fully superseded or eligible for archival.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -69,7 +66,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
|
||||
|
||||
**Keep forwarding compatibility bins.** Rejected: a forwarding executable remains another public launch name and compatibility promise. The pre-release repository can move callers directly to profiles.
|
||||
|
||||
**Put complete standalone Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. `dsh-base` plus thin app bundles gives shared policy one owner while retaining protocol-specific negative guarantees.
|
||||
**Put caller-supplied complete Cordis trees behind profile wrappers.** Rejected: that centralizes argv without centralizing application composition. Full profiles use `dsh-base` plus thin app bundles so shared policy has one owner. A repository-owned, versioned standalone bundle is allowed only when an explicit roster is the product behavior, as [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.md) records.
|
||||
|
||||
**Accept inline plugins or a complete `cordis.yml` in the TypeScript constructor.** Rejected: the SDK would become another package installer and application composer. Named profiles and patch files already provide persistent and per-launch customization through one resolution model.
|
||||
|
||||
@@ -83,19 +80,19 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
|
||||
|
||||
## Verification
|
||||
|
||||
- Source and built CLI acceptance cover `sdk` and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal.
|
||||
- Source and built CLI acceptance cover `sdk`, `sdk-minimal`, and `acp` help, transport startup, stdout purity, EOF, signals, and root disposal.
|
||||
- Bundle configuration tests pin module HMR disabled in `dsh-base` and absent from shipped mode overrides; the custom live-profile e2e pins config reload through the launcher's watch-only fallback.
|
||||
- Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources.
|
||||
- Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime.
|
||||
- The real-API workflow caps file parallelism at four because one profile e2e file can own several complete `dsh` subprocess trees; workflow tests pin that resource bound.
|
||||
- The Python suite exercises exe and node carriers; all packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes retain the existing artifact names.
|
||||
- The Python suite exercises exe and node carriers; packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes pin the `deepseek-harness-sdk-runtime-*` artifacts and profile launch.
|
||||
- `verify-application-entrypoints` includes invalid fixtures for package bins, executable sources, package-launching demo wrappers, and unclassified demos.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A user changes an SDK application's plugin composition through a named profile and ordered patches, using the same installation and resolution model as every other dsh application.
|
||||
- A custom profile receives live config watching without server module HMR and opts into source-module replacement only through an explicit row override.
|
||||
- SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly.
|
||||
- The full SDK and ACP profiles share the complete base application and one set of policy and tools; `sdk-minimal` owns its explicit standalone roster, and snapshots present intentional assembled differences.
|
||||
- Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime.
|
||||
- Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition.
|
||||
- Python keeps a visibly private, narrowly allowed direct-config carrier until its platform artifact migration is independently proven.
|
||||
- Python packages the ordinary `dsh` profile launcher while retaining a closed native runtime and no system-Node requirement for wheel users.
|
||||
|
||||
+16
-19
@@ -8,19 +8,19 @@ Status: implemented
|
||||
|
||||
DeepSeek Harness 应用进程需要由同一个机制负责组合、插件解析、环境发现、关闭和用户自定义。带完整 `cordis.yml` 的专用应用 bin 会在 profile 启动之外形成第二套生命周期:安装到 profile 的插件无法到达它,行为会与 `dsh-base` 偏离,SDK 调用方还需要学习任意进程 argv,而不是产品的组合模型。
|
||||
|
||||
Python SDK 分发一个原生可执行文件和三个平台 wheel 包;其中嵌入的直读配置运行时只有在重建并验证完整 VFS 闭包后才能改变启动架构。该分发需要一个明确的临时例外,而不是另一种通用 Node 应用模式。
|
||||
Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打包进程必须使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据。
|
||||
|
||||
## Decision
|
||||
|
||||
### 启动范围
|
||||
|
||||
所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。
|
||||
所有受支持的 Node 应用都通过 `dsh` CLI 与一个具名 profile 启动。随附应用命令是 `dsh web`、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`;`dsh web` 是刻意为 `--profile web` 保留的便捷别名,不是另一个应用入口。
|
||||
|
||||
Vendor CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于应用启动清单。包应用 bin 或直接启动包入口的根 demo 都不是可接受的扩展点。
|
||||
|
||||
### Profile 应用
|
||||
|
||||
`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。
|
||||
`@deepseek-ai/dsh-sdk-app` 与 `@deepseek-ai/dsh-acp-app` 在 `@deepseek-ai/dsh-base` 之上组合完整协议应用。SDK 组合包增加 JSON-RPC 服务器、应用自有帮助和 stdio 生命周期;ACP 组合包增加仅用于自动化的 ACP 服务器与相同的应用职责。两者都采用 base 层的模型、工具、持久化、settings、credentials、策略和环境行为。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)复用 SDK 启动与 JSON-RPC 服务,但刻意拥有不含 `dsh-base` 的完整显式配置树。
|
||||
|
||||
Profile manifest 负责 patch 重载:
|
||||
|
||||
@@ -29,11 +29,12 @@ Profile manifest 负责 patch 重载:
|
||||
| `web` | `live` |
|
||||
| `headless` | `startup` |
|
||||
| `sdk` | `startup` |
|
||||
| `sdk-minimal` | `startup` |
|
||||
| `acp` | `startup` |
|
||||
|
||||
自定义 profile 默认为 `live`。`startup` profile 仍会应用组合包、profile、home 级与调用时 `--patch` 各层,但启动后不会监视这些文件。`dsh-base` 插入的模块 HMR(热模块替换)配置项默认禁用;具有经过验证的源码模块重载生命周期的 profile 必须显式启用它。随附 profile 均不启用服务器模块 HMR:`patchReload: live` 使用启动器的仅配置 watcher,`startup` profile 则不安装 watcher。SDK 与 ACP 无法在一个自有 stdio 连接内安全替换其服务器、agent、持久化或工具注册表。
|
||||
|
||||
随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。模型可见工具与持久化默认值来自 `dsh-base`,可运行快照负责钉住这些已组装的应用输出。
|
||||
随附协议 profile 将 stdout 保留给协议帧,显示帮助时不启动 transport,并通过有界根节点 dispose(资源释放)处理 stdin EOF 与信号。ACP 继续仅用于自动化。SDK JSON-RPC 方法、通知字段与 `initialize.serverInfo.name` 保持稳定。完整 profile 的模型可见工具与持久化默认值来自 `dsh-base`;`sdk-minimal` 拥有自己的显式默认值。可运行快照负责固定已组装的应用输出。
|
||||
|
||||
### TypeScript SDK 自定义
|
||||
|
||||
@@ -43,25 +44,21 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
|
||||
|
||||
直接使用 SDK 时遵循普通 Harness home 解析:显式 `dshHome`、继承的 `DSH_HOME`,最后是 `~/.dsh`。`subagent-dsh-sdk` 则要求显式绝对 home,因此嵌套运行时不会通过操作系统 home 发现个人 profile、已安装插件、凭据或会话。DSH 专用 ACP 子进程示例同样传入隔离 home;ACP 后端自身继续适用于非 DSH agent。
|
||||
|
||||
### Python 例外与命名
|
||||
### Python 运行时
|
||||
|
||||
Python SDK 的直读配置应用位于私有 `packages/sdk/python-runtime` 包,名称是 `@deepseek-ai/dsh-sdk-python-runtime`。它唯一的打包可执行入口是 `lib/packaged-bin.js`,由私有 `dsh-sdk-python-runtime-closure` 部署根消费。它没有公开 npm bin。可运行的直启 Python 示例是 `examples/python-sdk-agent`。
|
||||
Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;可运行 Python 示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。
|
||||
|
||||
Python 可观察行为保持不变:Python API、SDK 协议格式、默认 `cordis.yml`、环境变量、wheel 包分发名称、打包可执行文件名称、伴随文件名称、显式运行时选项、零配置行为与支持平台。稳定 SDK 包族继续是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol`、`@deepseek-ai/dsh-sdk-jsonrpc-server`,协议 identity 继续是 `deepseek-harness-sdk-runtime`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。
|
||||
可执行文件族是 `deepseek-harness-sdk-runtime-<platform>-<arch>`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。
|
||||
|
||||
### 强制校验
|
||||
|
||||
`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具、测试支持以及私有 Python 载体进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。
|
||||
|
||||
## 暂缓的 Python 迁移
|
||||
|
||||
Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`,保持 wheel 包的封闭依赖与原生伴随文件行为,并删除 `@deepseek-ai/dsh-sdk-python-runtime`。只有这些条件在 Linux x64、Linux arm64 与 macOS arm64 全部通过后,可执行文件族才会从 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 改名为 `deepseek-harness-sdk-runtime-<platform>-<arch>`。临时载体与当前产物名称使这项义务清晰可见,同时不削弱当前 Python 兼容性。
|
||||
`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具和测试支持进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。
|
||||
|
||||
## 既有决策与取代关系
|
||||
|
||||
本决策取代 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)、[TypeScript SDK 客户端与 SDK subagent 后端](../feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md)、[移除 SDK 项目工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.zh.md)和[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的应用启动与包名事实。这些 Note 对 profile 分层、客户端/协议语义、已删除的项目工具链与原生打包仍分别具有独立权威。
|
||||
|
||||
[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。
|
||||
[ACP 仅自动化协议](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)继续负责 ACP 协议格式与交互范围。[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)继续负责基于角色的包名。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)部分取代本 Note 的 base 优先规则与完整配置树替代方案,同时保留本 Note 对 launcher 所有权的决策。没有任何活跃 Note 被完全取代,也没有 Note 符合归档条件。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -69,7 +66,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
|
||||
|
||||
**保留转发兼容 bin。** 拒绝:转发可执行文件仍然形成另一个公开启动名称与兼容承诺。预发布仓库可以让调用方直接迁移到 profile。
|
||||
|
||||
**把完整独立 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。`dsh-base` 加轻量应用组合包让共享策略只有一个归属,同时保留协议专属的负面保证。
|
||||
**把调用方提供的完整 Cordis 树放到 profile wrapper 后面。** 拒绝:这只集中 argv,没有集中应用组合。完整 profile 使用 `dsh-base` 加轻量应用组合包,使共享策略只有一个归属。只有当显式清单本身属于产品行为时,才允许仓库自有且有版本的独立组合包,具体见 [sdk-minimal](2026-08-24-standalone-sdk-minimal-profile.zh.md)。
|
||||
|
||||
**在 TypeScript 构造函数中接受内联插件或完整 `cordis.yml`。** 拒绝:SDK 会因此成为另一个包安装器和应用组合器。具名 profile 与 patch 文件已通过统一解析模型提供持久与逐次启动自定义。
|
||||
|
||||
@@ -83,19 +80,19 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
|
||||
|
||||
## 验证
|
||||
|
||||
- 源码与构建后 CLI 验收覆盖 `sdk` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。
|
||||
- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不再重复该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。
|
||||
- 源码与构建后 CLI 验收覆盖 `sdk`、`sdk-minimal` 和 `acp` 的帮助、transport 启动、stdout 纯净性、EOF、信号与根节点 dispose。
|
||||
- 组合包配置测试钉住 `dsh-base` 默认禁用模块 HMR,随附模式覆盖层不含该策略;自定义 live profile 的 e2e 钉住启动器仅监视 fallback 提供的配置重载。
|
||||
- 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。
|
||||
- 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。
|
||||
- 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。
|
||||
- Python 套件同时测试 exe 与 node 载体;全部打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试都保留既有产物名称。
|
||||
- Python 套件同时测试 exe 与 node 载体;打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试会钉住 `deepseek-harness-sdk-runtime-*` 产物与 profile 启动。
|
||||
- `verify-application-entrypoints` 包含包 bin、可执行源码、直启包的 demo wrapper 与未分类 demo 等非法 fixture(测试前置数据)。
|
||||
|
||||
## 影响
|
||||
|
||||
- 用户通过具名 profile 与有序 patch 更改 SDK 应用的插件组合,使用与其他所有 dsh 应用相同的安装与解析模型。
|
||||
- 自定义 profile 可以在不启用服务器模块 HMR 的情况下获得实时配置监视,只有显式覆盖配置项才会启用源码模块替换。
|
||||
- SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。
|
||||
- 完整 SDK 与 ACP profile 共享完整 base 应用和同一份策略与工具;`sdk-minimal` 拥有自己的显式独立清单,快照会呈现这些刻意采用的组装差异。
|
||||
- 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。
|
||||
- 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。
|
||||
- Python 保留一个清晰可见的私有直读配置载体,直到其平台产物迁移得到独立证明。
|
||||
- Python 打包普通 `dsh` profile 启动器,同时保留封闭原生运行时,wheel 用户无需系统 Node。
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-08-23-python-sdk-dsh-profile-runtime.md
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.md: 3298224688e8f0cd4216f8ed68d9e4364014d2a9
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 400e70113e1112aa6e4979c64a5046199c07e8a0
|
||||
@@ -0,0 +1,55 @@
|
||||
# Agent Note: Python SDK runtime through the dsh profile launcher
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Python SDK distributed a private Node application that booted a complete external `cordis.yml`, while every other supported application entered through `dsh` profiles. That exception duplicated environment loading, configuration ownership, plugin resolution, shutdown, artifact names, and test paths. It also made SDK customization an all-or-nothing application tree: a caller replacing one plugin had to own the JSON-RPC server and every unrelated deployment row.
|
||||
|
||||
A normal profile cannot be adopted only at the Python wrapper. The runtime executable must contain the `dsh` CLI, shipped profile and bundle files, native libraries, and a module-resolution path that works when profile files and external plugins live outside pkg's virtual filesystem.
|
||||
|
||||
## Decision
|
||||
|
||||
### One application launcher
|
||||
|
||||
The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The runnable minimal example selects the shipped `sdk-minimal` profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path.
|
||||
|
||||
The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, a bounded initialization timeout, and optional turn/shutdown timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path.
|
||||
|
||||
Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME` in the child environment. The SDK never discovers `~/.dsh`. The selected home consistently owns profiles, external plugins, credentials, settings, and sessions.
|
||||
|
||||
### Plugin customization
|
||||
|
||||
Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile <name> ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/<name>/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A selected profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile.
|
||||
|
||||
The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) lists one repository-owned bundle that inserts its complete explicit tree without `dsh-base`. Its persistent Bash and string-replace editor are present by composition rather than a server filter; dynamic runtime context, workspace instructions, settings, managed credentials, telemetry, compaction, and every other base row are absent. The same runtime still packages the full `sdk` and `web` profiles as separate choices.
|
||||
|
||||
The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`.
|
||||
|
||||
### Executable packaging
|
||||
|
||||
The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime-<platform>-<arch>`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged.
|
||||
|
||||
Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
|
||||
|
||||
The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
This decision implements and supersedes the Python exception and deferred-migration sections of [the single dsh application launcher](2026-08-22-single-dsh-application-launcher.md). It supersedes the private application, external complete-config, artifact-name, and customization facts in [the single-file Python SDK runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md), which remains authoritative for pkg/SEA, wheel construction, native target validation, and publication. The [standalone sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.md) supersedes only this note's minimal-overlay realization. No active note is fully superseded, so none is archived.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep complete `cordis.yml` as an advanced escape hatch.** Rejected because it preserves a second application assembly and lets a caller bypass profile environment, plugin, and shutdown ownership.
|
||||
|
||||
**Silently use `~/.dsh` for compatibility.** Rejected because an SDK process must not inherit a person's plugins, credentials, settings, or sessions without an explicit choice.
|
||||
|
||||
**Copy the virtual dependency tree into every home.** Rejected because it duplicates hundreds of megabytes and loads a second Cordis instance. Export-preserving proxies are small and retain module identity.
|
||||
|
||||
**Bundle pnpm and Node package management into every SDK launch.** Rejected because installed plugins are deployment state, not per-turn runtime work. Only `dsh plugin` needs the external package manager.
|
||||
|
||||
## Consequences
|
||||
|
||||
Python callers configure the same profile vocabulary as TypeScript and direct CLI users, and arbitrary external bundles can extend an SDK profile without introducing another launcher. Homes are selected explicitly, complete-config and `session_root` parameters are unavailable, and the executable includes shared-library assets plus profile-module proxies. The full `sdk`, standalone `sdk-minimal`, and `web` applications remain separate profiles inside the same packaged CLI. The installed-wheel CI makes those package, profile, native, and provider paths release requirements rather than source-only assumptions.
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Agent Note: 通过 dsh profile 启动器运行 Python SDK 运行时
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-23-python-sdk-dsh-profile-runtime.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml`;其他所有受支持应用都从 `dsh` profile 进入。该例外重复了环境加载、配置所有权、插件解析、关闭流程、产物命名与测试路径。它还把 SDK 自定义变成全量替换应用树:只想替换一个插件的调用方也必须拥有 JSON-RPC server 和所有无关 deployment 配置项。
|
||||
|
||||
仅修改 Python wrapper 无法采用普通 profile。运行时可执行程序必须包含 `dsh` CLI、随附 profile 与 bundle 文件、原生库,以及在 profile 文件和外部插件位于 pkg 虚拟文件系统之外时仍可工作的模块解析路径。
|
||||
|
||||
## 决策
|
||||
|
||||
### 一个应用启动器
|
||||
|
||||
运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。可运行极简示例选择随附 `sdk-minimal` profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。
|
||||
|
||||
公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择、有界初始化 timeout,以及可选的轮次/关闭 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。
|
||||
|
||||
每次 Python 启动都要求显式 `dsh_home`,或子进程环境中的非空 `DSH_HOME`。SDK 绝不会发现 `~/.dsh`。所选 home 统一拥有 profile、外部插件、凭据、设置与会话。
|
||||
|
||||
### 插件自定义
|
||||
|
||||
持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile <name> ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/<name>/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。所选 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile。
|
||||
|
||||
[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只列出一个仓库自有组合包,该组合包会插入不含 `dsh-base` 的完整显式配置树。持久 Bash 与字符串替换 editor 通过组合存在,而不是通过 server 筛选;动态运行时上下文、workspace 指令、settings、托管凭据、遥测、compaction 与其他所有 base 配置项均不存在。同一运行时仍会把完整 `sdk` 与 `web` profile 作为独立选择打包。
|
||||
|
||||
运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。
|
||||
|
||||
### 可执行程序打包
|
||||
|
||||
零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime-<platform>-<arch>`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。
|
||||
|
||||
普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
|
||||
|
||||
已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。
|
||||
|
||||
## 既有决策与取代关系
|
||||
|
||||
本决策实现并取代[单一 dsh 应用启动器](2026-08-22-single-dsh-application-launcher.zh.md)中的 Python 例外与延后迁移章节。它取代[单文件 Python SDK 运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的私有应用、外部完整配置、产物名称与自定义事实;后者继续负责 pkg/SEA、wheel 构建、原生目标验证与发布。[独立 sdk-minimal profile](2026-08-24-standalone-sdk-minimal-profile.zh.md)只取代本 Note 中的极简 overlay 实现。没有任何 active note 被完全取代,因此无需归档。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把完整 `cordis.yml` 保留为高级逃生口。** 不予采用,因为它会保留第二套应用组装,并允许调用方绕过 profile 的环境、插件与关闭所有权。
|
||||
|
||||
**为兼容性静默使用 `~/.dsh`。** 不予采用,因为 SDK 进程不应在没有显式选择时继承个人插件、凭据、设置或会话。
|
||||
|
||||
**把虚拟依赖树复制到每个 home。** 不予采用,因为这会重复数百 MB,并加载第二个 Cordis 实例。保留 exports 的代理很小,而且维持模块身份。
|
||||
|
||||
**把 pnpm 与 Node 包管理纳入每次 SDK 启动。** 不予采用,因为已安装插件属于 deployment 状态,而不是逐轮运行时工作。只有 `dsh plugin` 需要外部包管理器。
|
||||
|
||||
## 结果
|
||||
|
||||
Python 调用方使用与 TypeScript 和直接 CLI 用户相同的 profile 词汇,任意外部 bundle 可以扩展 SDK profile,而无需引入另一个 launcher。调用方必须显式选择 home,完整配置与 `session_root` 参数不可用,可执行程序则包含共享库资源和 profile 模块代理。完整 `sdk`、独立 `sdk-minimal` 与 `web` 应用作为同一打包 CLI 内的不同 profile 保持分离。Installed-wheel CI 将包、profile、原生与提供方路径变成发布要求,而不是仅在源码中成立的假设。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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-08-24-standalone-sdk-minimal-profile.md
|
||||
2026-08-24-standalone-sdk-minimal-profile.md: bc1a177dc4a232201004e6869caa452a7d55deb9
|
||||
2026-08-24-standalone-sdk-minimal-profile.zh.md: ae6fdb95079f748f26758a30c968c27548e9a87f
|
||||
@@ -0,0 +1,57 @@
|
||||
# Agent Note: Standalone sdk-minimal profile under dsh
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-24-standalone-sdk-minimal-profile.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A minimal SDK agent needs an explicit plugin roster. Expressing it as an overlay on the full `sdk` profile leaves every `dsh-base` service mounted and makes exclusion depend on filters and disable entries spread across unrelated plugins. A later base row can change runtime behavior even when the model-facing tools remain filtered.
|
||||
|
||||
A complete caller-supplied Cordis tree gives an exact roster but bypasses profile initialization, bundle resolution, persistent plugin management, home and invocation patch layers, and the `dsh`-owned process lifecycle. The minimal mode needs composition-level exclusion without creating another launcher or Python-owned application.
|
||||
|
||||
## Decision
|
||||
|
||||
### Launch and ownership
|
||||
|
||||
`dsh --profile sdk-minimal` is a shipped startup-only profile. Its manifest lists only `@deepseek-ai/dsh-sdk-minimal`; it does not list `@deepseek-ai/dsh-base`. The bundle inserts the complete Cordis tree over the launcher's empty profile root, while the profile patch, home patch, and ordered invocation patches retain their ordinary precedence above it.
|
||||
|
||||
The `dsh` CLI remains the only application launcher. The Python example selects `sdk-minimal` through the public `profile` field and an explicit Harness home. Python exposes no complete-config or arbitrary-argv path. The full Python and TypeScript SDK defaults remain `sdk`.
|
||||
|
||||
The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bounded shutdown. The startup provider accepts a profile-name config so both SDK profiles render their actual command without duplicating process lifecycle code.
|
||||
|
||||
### Explicit composition
|
||||
|
||||
The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`.
|
||||
|
||||
Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. This layer is POSIX-only because its persistent terminal uses Bash.
|
||||
|
||||
### Customization and Web
|
||||
|
||||
`dsh plugin --profile sdk-minimal add <package>` installs persistent dependencies and bundle layers. The profile's `cordis.patch.yml`, the home patch, and Python `patches` provide persistent, machine-local, and invocation-specific row changes. Customization can expand or replace the explicit tree, but it still passes through the same launcher and profile resolution.
|
||||
|
||||
The Python runtime continues to package `dsh-web-app` and the frontend assets. `dsh web` starts that separate browser application from the installed wheel; a Python SDK client cannot select `web` because it contains no JSON-RPC server row.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
This decision partially supersedes the base-first and standalone-tree rejection in [one dsh launcher for application profiles](2026-08-22-single-dsh-application-launcher.md). Repository-owned, versioned standalone profile bundles are allowed when an explicit roster is the product behavior; caller-supplied complete trees and alternate executables remain rejected.
|
||||
|
||||
It also supersedes the minimal-overlay realization in [Python SDK runtime through the dsh profile launcher](2026-08-23-python-sdk-dsh-profile-runtime.md) and the base-first default-profile statement in [profile plugin bundles](2026-08-05-profile-plugin-bundles.md). Those notes retain independent authority for launcher ownership, Python packaging and home requirements, general profile layering, and plugin management. No active note is fully superseded or eligible for archival.
|
||||
|
||||
## Verification
|
||||
|
||||
The bundle test pins the exact row and dependency roster. Profile-template and config-dump tests pin the one-bundle manifest, startup-only lifecycle, absence of `dsh-base`, and absence of module HMR. The keyless Python example test boots the real `dsh --profile sdk-minimal` process and asserts the generated manifest, complete system prompt, and two advertised tools. The installed-wheel minimal scenario exercises persistent shell state, editor effects, JSONL persistence, and the committed model-visible snapshot through the packaged executable.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the minimal mode as an overlay on `sdk`.** Rejected because filtering model-visible tools does not remove base services, prompt contributors, persistence choices, or later runtime behavior. It also makes the minimal application depend on controls in shared SDK server and system-prompt interfaces.
|
||||
|
||||
**Restore a Python `cordis` argument or environment-selected complete config.** Rejected because it recreates a Python-owned application composition and bypasses profile plugin management and launcher lifecycle.
|
||||
|
||||
**Create a second minimal SDK startup plugin.** Rejected because profile-aware help is the only variation; the SDK startup provider can own that config while keeping EOF and shutdown behavior local.
|
||||
|
||||
**Remove Web packages from the Python runtime closure.** Rejected because the wheel distributes the ordinary `dsh` application and Python deployments may also need `dsh web`; profile selection, not packaging divergence, separates those applications.
|
||||
|
||||
## Consequences
|
||||
|
||||
The minimal model and runtime roster changes only when its owning bundle changes or a trusted higher patch expands it. The price is deliberate duplication of a small complete application tree and omission of shared settings, credentials, policy controls, telemetry, and Web capabilities from that profile. Users choose the full `sdk` profile when they need those services, while both choices keep one launcher, one profile vocabulary, and one packaged runtime.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Agent Note: dsh 下的独立 sdk-minimal profile
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-24-standalone-sdk-minimal-profile.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
极简 SDK agent 需要显式插件清单。若把它表达为完整 `sdk` profile 上的 overlay,所有 `dsh-base` 服务仍保持挂载,排除逻辑则依赖分散在无关插件中的筛选器与 disable 配置项。以后新增的 base 配置项即使没有进入面向模型的工具清单,也可能改变运行时行为。
|
||||
|
||||
由调用方提供完整 Cordis 配置树可以得到确切清单,但会绕过 profile 初始化、组合包解析、持久插件管理、home 与调用 patch 层,以及由 `dsh` 拥有的进程生命周期。极简模式需要在组合层排除功能,同时不能创建另一个 launcher 或 Python 自有应用。
|
||||
|
||||
## 决策
|
||||
|
||||
### 启动与所有权
|
||||
|
||||
`dsh --profile sdk-minimal` 是随附的仅启动时 profile。其 manifest 只列出 `@deepseek-ai/dsh-sdk-minimal`,不列出 `@deepseek-ai/dsh-base`。该组合包在 launcher 的空 profile 根之上插入完整 Cordis 配置树,而 profile patch、home patch 与有序调用 patch 仍在其上保持普通优先级。
|
||||
|
||||
`dsh` CLI 仍是唯一应用 launcher。Python 示例通过公开 `profile` 字段与显式 Harness home 选择 `sdk-minimal`。Python 不暴露完整配置或任意 argv 路径。完整 Python 与 TypeScript SDK 的默认值仍是 `sdk`。
|
||||
|
||||
该组合包复用 `@deepseek-ai/dsh-sdk-app` 提供命令 help、stdin EOF 与有界关闭。启动提供方接受 profile 名称配置,因此两个 SDK profile 都能呈现自己的实际命令,且无需复制进程生命周期代码。
|
||||
|
||||
### 显式组合
|
||||
|
||||
该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。
|
||||
|
||||
Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。由于持久终端使用 Bash,此层只支持 POSIX。
|
||||
|
||||
### 自定义与 Web
|
||||
|
||||
`dsh plugin --profile sdk-minimal add <package>` 安装持久依赖与组合包层。Profile 自身的 `cordis.patch.yml`、home patch 与 Python `patches` 分别提供持久、机器本地和逐次调用的配置项变更。自定义可以扩展或替换显式配置树,但仍经过同一个 launcher 与 profile 解析。
|
||||
|
||||
Python 运行时继续打包 `dsh-web-app` 与前端产物。`dsh web` 会从已安装 wheel 启动这个独立浏览器应用;Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。
|
||||
|
||||
## 既有决策与取代关系
|
||||
|
||||
本决策部分取代[应用 profile 使用同一个 dsh launcher](2026-08-22-single-dsh-application-launcher.zh.md)中的 base 优先规则与独立配置树否决。显式清单属于产品行为时,可以使用仓库自有且有版本的独立 profile 组合包;由调用方提供的完整配置树与替代可执行程序仍被否决。
|
||||
|
||||
本决策也取代 [Python SDK 运行时通过 dsh profile launcher 启动](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)中的极简 overlay 实现,以及 [profile 插件组合包](2026-08-05-profile-plugin-bundles.zh.md)中默认 profile 均以 base 开头的表述。这些 Agent Note 对 launcher 所有权、Python 打包与 home 要求、普通 profile 分层及插件管理仍保持独立权威。没有活跃 Agent Note 被完全取代或符合归档条件。
|
||||
|
||||
## 验证
|
||||
|
||||
组合包测试固定确切配置项与依赖清单。Profile 模板与配置 dump 测试固定单组合包 manifest、仅启动时生命周期、`dsh-base` 缺席与模块 HMR 缺席。Keyless Python 示例测试启动真实 `dsh --profile sdk-minimal` 进程,并断言生成的 manifest、完整系统提示词与两个对外公布的工具。Installed-wheel 极简场景通过打包可执行程序验证持久 shell 状态、editor 文件效果、JSONL 持久化与已提交的模型可见快照。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**继续把极简模式作为 `sdk` 上的 overlay。** 否决:筛选面向模型的工具不会移除 base 服务、提示词贡献方、持久化选择或后续运行时行为,还会让极简应用依赖共享 SDK server 与系统提示词接口中的控制项。
|
||||
|
||||
**恢复 Python `cordis` 参数或由环境选择的完整配置。** 否决:这会重新创建 Python 自有应用组合,并绕过 profile 插件管理与 launcher 生命周期。
|
||||
|
||||
**创建第二个极简 SDK 启动插件。** 否决:唯一变化是 profile 感知的 help;SDK 启动提供方可以拥有该配置,同时把 EOF 与关闭行为保持在一处。
|
||||
|
||||
**从 Python 运行时闭包移除 Web 包。** 否决:wheel 分发普通 `dsh` 应用,而且 Python 部署也可能需要 `dsh web`;这些应用由 profile 选择隔离,而不是由打包差异隔离。
|
||||
|
||||
## 后果
|
||||
|
||||
极简模型与运行时清单只有在所属组合包变化,或受信任的上层 patch 扩展它时才会变化。代价是刻意重复一棵较小的完整应用树,并在该 profile 中省略共享 settings、凭据、策略控制、遥测与 Web 功能。需要这些服务的用户选择完整 `sdk` profile;两种选择仍共用一个 launcher、一套 profile 词汇与一个打包运行时。
|
||||
+2
-2
@@ -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/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: 47a1bbe9f8f4875e2437b3ff955663c4b3de7dce
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 0fab1e1f23a314bec80b5fd355abcb2881c1b1a5
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: 2e9a3e56252f8e91008a5559ad738a7ca678446b
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 31df6ebfbc15f35208bc73b391725b2039fe7819
|
||||
|
||||
@@ -22,7 +22,7 @@ The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace at
|
||||
|
||||
System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path.
|
||||
|
||||
The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) is the complete two-tool composition for the bundled JSON-RPC runtime. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point.
|
||||
The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) is the complete two-tool composition for `dsh --profile sdk-minimal`. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction; the [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns its launcher and bundle placement. Its keyless SDK process test asserts the assembled system prompt and two-tool catalog, and the installed-wheel scenario executes persistent Bash across calls and exercises the editor; the Python SDK tutorial provides the runnable entry point.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,4 +36,4 @@ The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/mini
|
||||
|
||||
## Consequences
|
||||
|
||||
The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and standalone JSON-RPC example state the same two-tool contract for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents.
|
||||
The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile state the same two-tool behavior for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents.
|
||||
|
||||
+3
-3
@@ -16,13 +16,13 @@ Status: implemented
|
||||
|
||||
preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。
|
||||
|
||||
进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。
|
||||
进程级 `core-web.cordis.yml` patch 缺席。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。
|
||||
|
||||
## 验证
|
||||
|
||||
系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。
|
||||
|
||||
独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 是内置 JSON-RPC 运行时的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无压缩选择。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。
|
||||
独立的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)是 `dsh --profile sdk-minimal` 的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md)说明其启动方式专属的环境配置、裸文件系统和无 compaction 选择;[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责其 launcher 与组合包位置。其无密钥 SDK 进程测试会断言组装后的系统提示词与双工具目录,installed-wheel 场景会跨调用执行持久 Bash 并使用编辑器;Python SDK 教程提供可运行入口。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -36,4 +36,4 @@ preset persona 恰好是 `You are a helpful software engineer assistant.`,它
|
||||
|
||||
## 后果
|
||||
|
||||
Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与独立 JSON-RPC 示例分别在各自的启动路径声明相同的双工具约定。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。
|
||||
Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 分别为各自启动路径声明相同的双工具行为。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。
|
||||
|
||||
+2
-2
@@ -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/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: eda9d3a3de91944a298070d6cc22f632294f7a28
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 1a72c6b1cdb7453b8468d6e6be37aec76323f714
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 37c341e964b556c7ab5fdd9081416883066b97d1
|
||||
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: e97e951028de3bcda9fe11be0351072481c72dd9
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `dshBin`/profile/patch/home config selects an isolated SDK application, `provider`/`model` feeds the child's `initialize`, and `env` supplies explicit child-only values such as its API key.
|
||||
- **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
|
||||
|
||||
`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical); the private `@deepseek-ai/dsh-sdk-python-runtime` carrier consumes the shared protocol through its packaged closure.
|
||||
`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical). TypeScript and Python clients both consume the shared protocol through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
|
||||
- **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`provider`/`model` 写入子进程 `initialize`,`env` 则提供子进程专用的显式值,例如其 API key。
|
||||
- **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。
|
||||
|
||||
`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致);私有 `@deepseek-ai/dsh-sdk-python-runtime` 载体通过其打包闭包消费共享协议。
|
||||
`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致)。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 消费共享协议;Python wheel 会打包该 CLI 及其封闭依赖树。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -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/feature/2026-07-31-web-telemetry-default-mount.md
|
||||
2026-07-31-web-telemetry-default-mount.md: 9399cbced62e326a46058a5f0c21c949e8737eff
|
||||
2026-07-31-web-telemetry-default-mount.zh.md: 133c5293fce676567a9ef8d2a47196678f4dc0e4
|
||||
2026-07-31-web-telemetry-default-mount.md: a492356eccba9f272ee216777eb518750c7b6b62
|
||||
2026-07-31-web-telemetry-default-mount.zh.md: 3d852229c069ae32f328c8dae38cfdf1744c7293
|
||||
|
||||
@@ -10,7 +10,7 @@ The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry
|
||||
|
||||
## Decision
|
||||
|
||||
The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every profile has one consistent telemetry capability. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps that row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound.
|
||||
The shared dsh base bundle (`packages/bundle/base/cordis.patch.yml`) mounts the `session-telemetry-otel` row with a baked-in production endpoint, so every base-backed profile has one consistent telemetry capability. The standalone [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) deliberately omits that row. The [default-off decision](2026-08-10-telemetry-default-off.md) keeps the mounted row in `DISABLED` mode unless a deployment explicitly selects `FULL` or `FEEDBACK_ONLY`; the endpoint alone does not authorize reporting. Web and headless use the [bounded, escalating process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) on SIGINT/SIGTERM, giving an enabled backend's three-second shutdown deadline time to drain before the five-second launcher bound.
|
||||
|
||||
| Ruling | Value | Rationale |
|
||||
|---|---|---|
|
||||
@@ -27,7 +27,7 @@ The base bundle test pins the shipped `DISABLED` mode expression, the backend su
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives all profiles one patch target without authorizing any upload.
|
||||
**No default mount; deployments add the row themselves.** Rejected because the mounted `DISABLED` mode retains a local feedback warning and gives every base-backed profile one patch target without authorizing any upload.
|
||||
|
||||
**A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个 profile 都具有一致的遥测能力。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让该配置行保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。
|
||||
共享 dsh 基础组合包(`packages/bundle/base/cordis.patch.yml`)挂载带有内置生产 endpoint 的 `session-telemetry-otel` 配置行,使每个基于 base 的 profile 都具有一致的遥测能力。独立的 [`sdk-minimal` profile](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)刻意省略该配置项。[默认关闭决策](2026-08-10-telemetry-default-off.zh.md)让已挂载配置项保持 `DISABLED` 模式,除非部署方显式选择 `FULL` 或 `FEEDBACK_ONLY`;仅配置 endpoint 不构成上报授权。Web 与 headless 在 SIGINT/SIGTERM 时使用[有界、可升级的进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md),在启动器 5 秒上限到期前,先给已启用的后端 3 秒关闭截止时间完成排空。
|
||||
|
||||
| 决策项 | 取值 | 理由 |
|
||||
|---|---|---|
|
||||
@@ -28,7 +28,7 @@ Status: implemented
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为所有 profile 提供同一个 patch 目标,同时不授权任何上传。
|
||||
**默认不挂载,部署方自行添加配置行。** 不采用:挂载的 `DISABLED` 模式会保留本地反馈警告,并为每个基于 base 的 profile 提供同一个 patch 目标,同时不授权任何上传。
|
||||
|
||||
**开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。
|
||||
|
||||
|
||||
+2
-2
@@ -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/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md
|
||||
2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 7d068aebb6642602aac0a039c8635acf555ccfe8
|
||||
2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: c32a9a2e09ff0acda592062f780427c636fd65dd
|
||||
2026-08-11-minimal-profiles-bare-two-tool-runtime.md: 6ea86832b632c631b7e02d6c486f3858fd2632a4
|
||||
2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 73f6f16a51c14a7c98915a84878b39596d12f245
|
||||
|
||||
+6
-6
@@ -12,17 +12,17 @@ The two launch paths also have different configuration owners. Web mounts a per-
|
||||
|
||||
## Decision
|
||||
|
||||
Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. Sandbox and approval services remain mounted and enforce their policies; only their model-facing dynamic context is absent.
|
||||
Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. The Web host retains its sandbox and approval services; the standalone profile mounts a danger-full-access sandbox policy and no approval service. Neither contributes model-facing policy context.
|
||||
|
||||
The standalone [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) remains a complete JSON-RPC process composition. It mounts `dsh-sdk-jsonrpc-server`, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the deployment's danger-full-access sandbox policy; the editor is not confined by that policy.
|
||||
The standalone [`@deepseek-ai/dsh-sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) remains a complete JSON-RPC process composition behind `dsh --profile sdk-minimal`. It mounts SDK startup and JSON-RPC serving, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence under `$DSH_HOME/sessions`. It does not mount `token-meter`, `compaction-basic`, `fs-sandbox`, or `fs-observation-policy`. Persistent Bash still consumes the profile's danger-full-access sandbox policy; the editor is not confined by that policy. The [standalone-profile decision](../architecture/2026-08-24-standalone-sdk-minimal-profile.md) owns this bundle placement and its separation from `dsh-base`.
|
||||
|
||||
`DSH_SYSTEM_PROMPT` selects the standalone persona. `DSH_MODEL` names the DeepSeek provider catalog entry, and `DSH_CONTEXT_WINDOW` supplies that entry's capacity. Because the SDK client owns the JSON-RPC `initialize` request, [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) also uses `DSH_MODEL` as its default `model` argument; an explicit `--model` remains authoritative. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path.
|
||||
`DSH_SYSTEM_PROMPT` selects the standalone persona, and `DSH_CONTEXT_WINDOW` supplies fallback capacity for a model without exact catalog metadata. The SDK client's JSON-RPC `initialize` request is the sole runtime model selection. [`minimal.py`](../../../../examples/python-sdk-agent/minimal.py) may read `DSH_MODEL` only as the command's default `model` argument; an explicit `--model` needs no matching child environment value. Endpoint and credential variables stay owned by the DeepSeek adapter's existing environment-resolution path.
|
||||
|
||||
## Verification
|
||||
|
||||
The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services.
|
||||
|
||||
The SDK replay boots the real JSON-RPC agent process through the SDK client, injects an environment-selected prompt, asserts the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message, and executes both tools. Python SDK bundled-runtime coverage initializes the standalone configuration through each available packaged carrier with environment-selected model, model capacity, and prompt values. Cordis validation checks that both configurations resolve their declared plugins and configuration fields.
|
||||
The SDK keyless process test boots real `dsh --profile sdk-minimal`, injects an environment-selected prompt, and asserts the generated one-bundle manifest, assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message. Python SDK bundled-runtime coverage initializes the standalone profile through each available packaged carrier with environment-selected model, model capacity, and prompt values, then executes both tools. Cordis validation checks that both configurations resolve their declared plugins and configuration fields.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -32,8 +32,8 @@ The SDK replay boots the real JSON-RPC agent process through the SDK client, inj
|
||||
|
||||
**Use one Cordis leaf for Web and Python SDK startup.** Rejected because a Web preset contributes agent-scoped services to an existing multi-session host, while the Python SDK must launch a complete process containing the JSON-RPC server and its process-wide dependencies.
|
||||
|
||||
**Read `DSH_MODEL` only inside Cordis.** Rejected because Cordis configures the provider catalog but does not own the SDK client's JSON-RPC `initialize` request. The launcher must pass the same model to the client request for the environment value to select the routed model.
|
||||
**Mirror the requested model into `DSH_MODEL`.** Rejected because the direct adapter accepts model ids outside its advisory catalog and resolves fallback context metadata for them. Mirroring creates two inputs for one selection; the SDK initialization request is authoritative, while `DSH_MODEL` remains only a convenience default in `minimal.py`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path continues to communicate only through the bundled stdio JSON-RPC runtime.
|
||||
Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path communicates only through the bundled `dsh` stdio JSON-RPC profile.
|
||||
|
||||
+6
-6
@@ -12,17 +12,17 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash`
|
||||
|
||||
## 决策
|
||||
|
||||
两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。沙箱与批准服务仍保持挂载并强制其策略;只有它们面向模型的动态上下文缺席。
|
||||
两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。Web 宿主保留沙箱与批准服务;独立 profile 挂载 danger-full-access 沙箱策略,不挂载批准服务。两者都不贡献面向模型的策略上下文。
|
||||
|
||||
独立的 [`minimal.cordis.yml`](../../../../examples/python-sdk-agent/minimal.cordis.yml) 仍是完整的 JSON-RPC 进程组合。它挂载 `dsh-sdk-jsonrpc-server`、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及未压缩的 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费部署的 danger-full-access 沙箱策略;编辑器不受该策略限制。
|
||||
独立的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)仍是 `dsh --profile sdk-minimal` 后面的完整 JSON-RPC 进程组合。它挂载 SDK 启动与 JSON-RPC 服务、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 持久化。它不挂载 `token-meter`、`compaction-basic`、`fs-sandbox` 或 `fs-observation-policy`。持久 Bash 仍消费该 profile 的 danger-full-access 沙箱策略;编辑器不受该策略限制。[独立 profile 决策](../architecture/2026-08-24-standalone-sdk-minimal-profile.zh.md)负责该组合包的位置及其与 `dsh-base` 的分离。
|
||||
|
||||
`DSH_SYSTEM_PROMPT` 选择独立组合的 persona。`DSH_MODEL` 命名 DeepSeek 提供方目录项,`DSH_CONTEXT_WINDOW` 提供该目录项的容量。由于 SDK 客户端拥有 JSON-RPC `initialize` 请求,[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)也使用 `DSH_MODEL` 作为 `model` 参数的默认值;显式 `--model` 仍具有最高优先级。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。
|
||||
`DSH_SYSTEM_PROMPT` 选择独立组合的 persona,`DSH_CONTEXT_WINDOW` 为没有确切目录元数据的模型提供后备容量。SDK 客户端的 JSON-RPC `initialize` 请求是唯一运行时模型选择。[`minimal.py`](../../../../examples/python-sdk-agent/minimal.py)可以只把 `DSH_MODEL` 读作命令的默认 `model` 参数;显式 `--model` 不需要匹配的子进程环境值。端点与凭据变量继续由 DeepSeek 适配器现有的环境解析路径持有。
|
||||
|
||||
## 验证
|
||||
|
||||
Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。
|
||||
|
||||
SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环境选择的提示词,断言组装提示词与精确双工具目录,另外断言不存在任何 system-prompt 拥有的 runtime-context 消息,并执行两个工具。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立配置。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。
|
||||
SDK keyless 进程测试启动真实 `dsh --profile sdk-minimal`,注入由环境选择的提示词,并断言生成的单组合包 manifest、组装提示词、精确双工具目录,以及不存在任何 system-prompt 拥有的 runtime-context 消息。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立 profile,然后执行两个工具。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -32,8 +32,8 @@ SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环
|
||||
|
||||
**为 Web 与 Python SDK 启动使用同一个 Cordis leaf。** 不予采用,因为 Web preset 向现有多会话宿主贡献 agent 作用域服务,而 Python SDK 必须启动包含 JSON-RPC 服务器及其进程级依赖的完整进程。
|
||||
|
||||
**只在 Cordis 内读取 `DSH_MODEL`。** 不予采用,因为 Cordis 配置提供方目录,但不拥有 SDK 客户端的 JSON-RPC `initialize` 请求。launcher 必须向客户端请求传递同一个模型,环境值才能选择路由模型。
|
||||
**把请求模型镜像到 `DSH_MODEL`。** 不予采用,因为直接适配器接受不在建议目录中的模型 id,并为它们解析后备上下文元数据。镜像会为同一项选择制造两个输入;SDK 初始化请求具有权威,`DSH_MODEL` 只保留为 `minimal.py` 的便捷默认值。
|
||||
|
||||
## 后果
|
||||
|
||||
Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径继续仅通过内置 stdio JSON-RPC 运行时通信。
|
||||
Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径只通过内置 `dsh` stdio JSON-RPC profile 通信。
|
||||
|
||||
+2
-2
@@ -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/testing/2026-08-13-python-minimal-model-visible-snapshot.md
|
||||
2026-08-13-python-minimal-model-visible-snapshot.md: 37c76be93fb4f18fa99ceec7c15d20e572f2dfb3
|
||||
2026-08-13-python-minimal-model-visible-snapshot.zh.md: 5c2b0dd5fcd6ec5ea3a68eb884e19b0917bbe0be
|
||||
2026-08-13-python-minimal-model-visible-snapshot.md: 1cef57c440ddd2209628016ab550174b448587bc
|
||||
2026-08-13-python-minimal-model-visible-snapshot.zh.md: fbbb3bd6f4c3edadb61a02eebbae3dfaa97cb8e7
|
||||
|
||||
+5
-7
@@ -6,15 +6,13 @@ English | [中文](2026-08-13-python-minimal-model-visible-snapshot.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Python lane never compared what the minimal composition actually shows the model. Dynamic runtime context reaches history as a user message, so the mock model's assertion that system-role messages equal the deployment persona could not see it, and the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name. The sandbox-policy runtime-context message therefore rode along in the checked-in [minimal composition](../../../../examples/python-sdk-agent/minimal.cordis.yml) while `python-runtime` stayed green, and any plugin that adds a system section, a tool, or another context message could do the same.
|
||||
The Python lane needs an exact record of what the standalone minimal profile shows the model. Functional tool assertions prove execution but do not reveal an added system section, tool description, or user-role context message, while the advanced executable snapshot replaces each request header's assembled system prompt with a token and each tool schema with its name.
|
||||
|
||||
## Decision
|
||||
|
||||
The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across the platforms the expected output replays on.
|
||||
The `sdk-minimal` scenario in [the packaged-runtime smoke](../../../../scripts/smoke-python-runtime.py) boots the shipped profile and records `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`: for every model request of the turn, the advertised tool schemas verbatim and the message list. System and user messages keep their full text with the scenario's temporary directory tokenized; assistant and tool messages keep only call identity, because their PTY and filesystem text differs across replay platforms. The profile omits dynamic runtime context, so every message it emits is compared.
|
||||
|
||||
One model-visible message is excluded: the agent loop's dynamic runtime-context snapshot. The same composition emits it on macOS and not on Linux, which the required lane runs, so no single expected output can carry it. That difference is a defect in its own right ([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488)) — this expected output covers every other model-visible message rather than waiting for it.
|
||||
|
||||
The mock model no longer asserts the minimal scenario's tools and system prompts — the snapshot owns that surface and reports a complete diff instead of the first mismatch. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`.
|
||||
The snapshot, rather than inline mock-model assertions, owns the minimal scenario's tools and system prompts and reports their complete diff. Snapshot comparison takes its directory and file set as arguments, so the `minimal` and `advanced` expected outputs use one implementation, and `--update-snapshots` accepts `sdk-minimal`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -22,12 +20,12 @@ The mock model no longer asserts the minimal scenario's tools and system prompts
|
||||
|
||||
**Extend the mock model's inline assertions.** Every new model-visible contribution would need another hand-written expectation, and a failure names one mismatch rather than the whole surface. Tool descriptions would also be duplicated from the composition into the script.
|
||||
|
||||
**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins the same composition's system prompt, tool schemas, and runtime context, but through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's closure assembles for a Python caller.
|
||||
**Rely on the TypeScript SDK snapshot.** Its `persistent-tools` scenario pins a similar two-tool composition through replayed model responses and a source or `lib` runtime, in a different required job. It cannot show what the deployed executable's shipped profile assembles for a Python caller.
|
||||
|
||||
## Consequences
|
||||
|
||||
A change to the minimal composition's model-visible surface — a system section, a tool, a tool description, or an added user message — now fails `python-runtime` with the exact diff, and landing it means rerunning `--scenario sdk-minimal --update-snapshots` and reviewing that diff. The minimal composition's tool descriptions become reviewed expected output.
|
||||
|
||||
Assistant and tool message text is no longer compared, and the runtime-context snapshot is not compared at all. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) owns the excluded message until its platform difference is resolved.
|
||||
Assistant and tool message text is not compared. The scenario's own assertions continue to own persistent-shell state, editor output, and the final response; the snapshot owns every model-visible message the profile emits.
|
||||
|
||||
[AGENTS.md](../../../../AGENTS.md) and [the testing policy](../../../../docs/testing.md) now name both SDKs as independent projections of the agent loop, session lifecycle, and `SessionEventMap`, so a change to any of those carries updating both expected outputs rather than only the one a contributor happens to run.
|
||||
|
||||
+5
-7
@@ -6,15 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
Python 通道从未比对极简组合实际展示给模型的内容。动态运行时上下文以 user 消息进入历史,因此 mock 模型"system 角色消息等于部署 persona"的断言看不见它;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符、把每个工具 schema 换成其名称。于是 sandbox-policy 的运行时上下文消息一直搭车留在签入的[极简组合](../../../../examples/python-sdk-agent/minimal.cordis.yml)里,而 `python-runtime` 始终是绿的;任何新增系统分段、工具或其他上下文消息的插件都能照此蒙混过关。
|
||||
Python 通道需要精确记录独立极简 profile 实际展示给模型的内容。功能性工具断言可以证明执行,但无法发现新增系统分段、工具描述或 user 角色上下文消息;而进阶可执行文件快照会把每个请求头中已组装的系统提示词换成占位符,并把每个工具 schema 换成其名称。
|
||||
|
||||
## 决策
|
||||
|
||||
[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在期望输出需要重放的各平台上并不相同。
|
||||
[打包运行时冒烟测试](../../../../scripts/smoke-python-runtime.py)的 `sdk-minimal` 场景会启动随附 profile,并录制 `scripts/snapshots/python-sdk-single-exe/minimal/model-visible.json`:对该回合的每个模型请求,逐字记录对外公布的工具 schema 与消息列表。system 与 user 消息保留全文,仅将场景的临时目录替换为占位符;assistant 与 tool 消息只保留调用标识,因为它们的 PTY 与文件系统文本在各回放平台上并不相同。该 profile 省略动态运行时上下文,因此它发出的每条消息都会参与比对。
|
||||
|
||||
有一条模型可见消息被排除在外:agent loop 的动态运行时上下文快照。同一组合在 macOS 上会发出它,在必需车道所用的 Linux 上不会,因此任何单一期望输出都无法承载它。该差异本身就是缺陷([#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488))——这份期望输出覆盖其余全部模型可见消息,而不是等它先被修复。
|
||||
|
||||
mock 模型不再断言极简场景的工具与系统提示词——该面由快照拥有,并给出完整差异而非首个不匹配项。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。
|
||||
极简场景的工具与系统提示词由快照拥有,而不是由 mock 模型内联断言;快照会给出其完整差异。快照比对以目录与文件集合为参数,因此 `minimal` 与 `advanced` 两份期望输出共用一套实现,且 `--update-snapshots` 接受 `sdk-minimal`。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -22,12 +20,12 @@ mock 模型不再断言极简场景的工具与系统提示词——该面由快
|
||||
|
||||
**扩展 mock 模型中的内联断言。** 每新增一项模型可见贡献都要再手写一条期望,且失败只会指出一处不匹配而非整个面。工具描述还会从组合复制进脚本,形成重复。
|
||||
|
||||
**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景固定了同一组合的系统提示词、工具 schema 与运行时上下文,但走的是重放的模型响应与 source 或 `lib` 运行时,且位于另一个必需任务中。它无法体现已部署可执行文件的闭包为 Python 调用方组装出什么。
|
||||
**依赖 TypeScript SDK 快照。** 其 `persistent-tools` 场景通过重放模型响应与 source 或 `lib` 运行时固定一套相似的双工具组合,且位于另一个必需任务中。它无法体现已部署可执行文件的随附 profile 为 Python 调用方组装出什么。
|
||||
|
||||
## 后果
|
||||
|
||||
极简组合模型可见面的改动——系统分段、工具、工具描述或新增的 user 消息——现在会让 `python-runtime` 带着精确差异失败;要让它落地,就必须重新运行 `--scenario sdk-minimal --update-snapshots` 并审阅该差异。极简组合的工具描述由此成为经过审阅的期望输出。
|
||||
|
||||
assistant 与 tool 消息文本不再参与比对,运行时上下文快照则完全不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;被排除的那条消息由 [#2488](https://github.com/deepseek-harness/deepseek-harness/issues/2488) 负责,直到其平台差异得到解决。
|
||||
assistant 与 tool 消息文本不参与比对。持久 shell 状态、编辑器输出与最终响应仍由该场景自身的断言拥有;快照负责该 profile 发出的每条模型可见消息。
|
||||
|
||||
[AGENTS.md](../../../../AGENTS.md) 与[测试政策](../../../../docs/testing.zh.md)现已点明两个 SDK 都是 agent loop、会话生命周期与 `SessionEventMap` 的独立投影,因此改动其中任何一项都要连带更新两侧的期望输出,而不只是贡献者恰好会运行的那一侧。
|
||||
|
||||
+2
-2
@@ -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/testing/2026-08-23-installed-python-wheel-black-box-ci.md
|
||||
2026-08-23-installed-python-wheel-black-box-ci.md: f2b5bd0edeb02a5d72e8010c62c3cfb59ee95c5d
|
||||
2026-08-23-installed-python-wheel-black-box-ci.zh.md: fb0f5fb2da676f1a24a2630bd45f005f46a3095e
|
||||
2026-08-23-installed-python-wheel-black-box-ci.md: a2fd4134d7bff0e74aa2d1afc3590e9cdd90809e
|
||||
2026-08-23-installed-python-wheel-black-box-ci.zh.md: 203440ed0f6bbe84257474dd69400a64bc480cd3
|
||||
|
||||
@@ -18,7 +18,7 @@ The black-box harness rejects a non-venv process, repository-relative working di
|
||||
|
||||
### Keyless behavior
|
||||
|
||||
Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default configuration, an external complete configuration, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, JSONL/Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories.
|
||||
Every target runs the complete packaged-runtime scenario set after installation. A local SSE model keeps outputs deterministic while the public SDK exercises the default SDK profile, ordered patch overlays, external bundle installation through `dsh plugin`, persistent PTY and editor behavior, worker-thread code and workflow execution, ripgrep-backed search, external stdio MCP discovery and execution, model-visible and durable snapshots, Zstandard persistence, direct JSON-RPC, and shutdown. A restart snapshot launches two complete SDK runtime processes against one persistence root and pins their isolated model histories, high-level results, and separate durable logs. The installed run replaces the source-SDK pre-wheel run; the executable and wheel are tested together once rather than maintaining two behavior inventories.
|
||||
|
||||
Linux additionally retains its manylinux 2.28 clean-install smoke and GLIBC checks. macOS retains deployment-target and native helper checks. These platform constraints supplement the common black-box behavior rather than substituting for it.
|
||||
|
||||
@@ -34,7 +34,7 @@ The pull-request `python-runtime` job calls the reusable builder for Linux x64,
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, the closed dependency set, native sidecars, wheel tags, and release artifacts.
|
||||
This decision supersedes the single-target topology in the archived [required Python runtime pull-request validation](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md) while retaining its requirement that the real executable, snapshots, wheels, and clean installation meet before merge. The [Python SDK dsh profile runtime](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.md) owns the launched application and customization surface; the [single-file Python SDK runtime distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) remains authoritative for SEA packaging, native sidecars, wheel tags, and release artifacts.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ Python SDK 单元测试驱动 fake peer,而打包运行时工作流可以在
|
||||
|
||||
### Keyless 行为
|
||||
|
||||
每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认配置、外部完整配置、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、JSONL/Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。
|
||||
每个目标都会在安装后运行完整的打包运行时场景。一个本地 SSE mock 模型提供确定性输出,公开 SDK 则覆盖默认 SDK profile、有序 patch overlay、通过 `dsh plugin` 安装外部 bundle、持久 PTY 与 editor 行为、worker thread 代码与 workflow 执行、基于 ripgrep 的搜索、外部 stdio MCP 发现与执行、模型可见及持久化快照、Zstandard 持久化、直接 JSON-RPC 与关闭。Restart 快照针对同一持久化根目录启动两个完整 SDK 运行时进程,并固定其彼此隔离的模型历史、高层结果与独立持久日志。安装后运行取代 wheel 构建前的源码 SDK 运行,因此可执行文件与 wheel 包共同接受一次验证,而不是维护两套行为清单。
|
||||
|
||||
Linux 另外保留 manylinux 2.28 干净安装冒烟测试与 GLIBC 检查。macOS 保留部署目标与原生 helper 检查。这些平台约束补充共同黑盒行为,不能替代它。
|
||||
|
||||
@@ -34,7 +34,7 @@ Fork 与 Dependabot 拉取请求永远不会获得仓库密钥。它们的原生
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)仍负责 SEA 打包、封闭依赖集合、原生 sidecar、wheel 包标签与发布产物。
|
||||
本决策取代已归档的[必需 Python 运行时拉取请求验证](../../archived/testing/2026-08-12-required-python-runtime-pull-request-ci.md)中的单目标拓扑,同时保留真实可执行文件、快照、wheel 包与干净安装必须在合并前相遇的要求。[Python SDK dsh profile 运行时](../architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责启动应用与自定义接口;[单文件 Python SDK 运行时 distribution](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)继续负责 SEA 打包、原生 sidecar、wheel 包标签与发布产物。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
platform="${TARGET#node24-}"
|
||||
exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform"
|
||||
exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform"
|
||||
[ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
|
||||
case "$platform" in
|
||||
linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ tmp/
|
||||
.idea
|
||||
mise.toml
|
||||
dist-exe/
|
||||
python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-*
|
||||
python/sdk-runtime/src/deepseek_harness_runtime/runtime/deepseek-harness-sdk-runtime-*
|
||||
python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/
|
||||
python/**/__pycache__/
|
||||
python/**/.pytest_cache/
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ sdk-wheel:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run verify-runtime-closure
|
||||
- pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets="$PKG_TARGET"
|
||||
- EXE="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$PLATFORM"
|
||||
- EXE="$PWD/dist-exe/deepseek-harness-sdk-runtime-$PLATFORM"
|
||||
- test -x "$EXE"
|
||||
- uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE"
|
||||
- python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM"
|
||||
|
||||
@@ -8,7 +8,7 @@ DeepSeek Harness is an all-plugin agent harness on vendored Cordis. Read [docs/a
|
||||
|
||||
## Application launch
|
||||
|
||||
Node apps launch only through `dsh` profiles; application-package bins, demos, and SDK argv escape hatches are forbidden. The private Python runtime is the sole temporary exception. [Architecture](docs/architecture.md#application-launch) owns scope and deferred artifact rename; `pnpm run verify-application-entrypoints` enforces it.
|
||||
Supported Node applications launch only through `dsh` profiles; application-package bins, demos, and public SDK argv escape hatches are forbidden. [Architecture](docs/architecture.md#application-launch) owns the launch set; `pnpm run verify-application-entrypoints` enforces it.
|
||||
|
||||
## Repository layout
|
||||
|
||||
@@ -46,7 +46,7 @@ packages/ @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
|
||||
acp/ automation-only Agent Client Protocol server
|
||||
interaction/ approval/interaction capabilities, permission, commands, ask-user
|
||||
boot/ shared profile/application boot glue
|
||||
sdk/ JSON-RPC protocol, server, TypeScript client, and private Python carrier
|
||||
sdk/ JSON-RPC protocol, server, and TypeScript client
|
||||
examples/ reusable demo bundles (agent-spine)
|
||||
experimental/ private prototypes excluded from official releases
|
||||
support/ dev/test infrastructure
|
||||
|
||||
@@ -88,6 +88,7 @@ External packages that a workspace package resolves at runtime. The tier covers
|
||||
| [`react`](https://github.com/facebook/react) | MIT |
|
||||
| [`react-dom`](https://github.com/facebook/react) | MIT |
|
||||
| [`readable-stream`](https://github.com/nodejs/readable-stream) | MIT |
|
||||
| [`resolve.exports`](https://github.com/lukeed/resolve.exports) | MIT |
|
||||
| [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 |
|
||||
| [`shiki`](https://github.com/shikijs/shiki) | MIT |
|
||||
| [`supports-color`](https://github.com/chalk/supports-color) | MIT |
|
||||
|
||||
@@ -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 apps/cli/README.md
|
||||
README.md: d59b8264093dafb0160893c942371a4daa942c14
|
||||
README.zh.md: 6a209f5ad64b38c138ae1712a05ed9e840d9a74f
|
||||
README.md: ff0efb03d8a5d6da747f1b5bc0d05361a6c8a567
|
||||
README.zh.md: 8e9c122afa87525c99e8045f220b96b4511c04fe
|
||||
|
||||
+4
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero.
|
||||
The `dsh` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command; the SDK defaults to `sdk`, and the minimal example selects `sdk-minimal`. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, configuration errors, and boot failures exit nonzero.
|
||||
|
||||
## Entry modes
|
||||
|
||||
@@ -12,10 +12,11 @@ The `dsh` command is the sole supported Node application launcher: profiles are
|
||||
| `dsh --profile acp` | Serve automation clients over ACP stdio until disconnect. |
|
||||
| `dsh --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. |
|
||||
| `dsh --profile sdk` | Serve SDK clients over JSON-RPC stdio until shutdown or disconnect. |
|
||||
| `dsh --profile sdk-minimal` | Serve SDK clients with the standalone minimal agent tree. |
|
||||
| `dsh web` | Alias of `--profile web`. |
|
||||
| `dsh plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
|
||||
|
||||
The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
|
||||
The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`.
|
||||
|
||||
## App arguments
|
||||
|
||||
@@ -38,7 +39,7 @@ The tree composes over an empty root:
|
||||
- then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml`
|
||||
- then `--patch` overlays
|
||||
|
||||
Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
|
||||
Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
|
||||
|
||||
Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
|
||||
`dsh` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP 都是 profile,而不是独立的公开 bin。Python 运行时 wheel 会打包同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。
|
||||
|
||||
## 入口模式
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
| `dsh --profile acp` | 通过 ACP stdio 为自动化 client 提供服务,直至断开连接。 |
|
||||
| `dsh --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 |
|
||||
| `dsh --profile sdk` | 通过 JSON-RPC stdio 为 SDK client 提供服务,直至关闭或断开连接。 |
|
||||
| `dsh --profile sdk-minimal` | 以独立极简 agent 配置树为 SDK client 提供服务。 |
|
||||
| `dsh web` | `--profile web` 的别名。 |
|
||||
| `dsh plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
|
||||
|
||||
运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
|
||||
运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。
|
||||
|
||||
## 应用参数
|
||||
|
||||
@@ -40,7 +41,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以
|
||||
- profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml`
|
||||
- `--patch` 指定的覆盖层
|
||||
|
||||
`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。
|
||||
`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。
|
||||
|
||||
使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# DSH Base Composition
|
||||
|
||||
The dsh-base bundle patch every profile applies first; mode bundles (dsh-web-app, dsh-headless) and the user's profile layer patch over it.
|
||||
The dsh-base bundle patch shared by the web, headless, sdk, and acp profiles; their mode bundles and user layers patch over it, while sdk-minimal owns a separate standalone tree.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-app": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-minimal": "workspace:^",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
|
||||
|
||||
@@ -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 apps/cli/reference/README.md
|
||||
README.md: 0f407afa3b06d144681550d5096bf96c498e6451
|
||||
README.zh.md: bf4dc4ca9f49c1801d108234411123de459c0444
|
||||
README.md: 33de399dc4b2b8e67ed24ed046fcc0da2ff0a7ac
|
||||
README.zh.md: 0f20245f318e31159780d29cca955fc85a5b5481
|
||||
|
||||
@@ -8,9 +8,9 @@ This reference defines the profile, web-alias, plugin-management, and config-dum
|
||||
|
||||
`dsh --profile <name>` boots the profile at `$DSH_HOME/profiles/<name>`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch <path>` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. `dsh.profile.patchReload` selects `live` patch-file watching or `startup` one-time loading; omission defaults a custom profile to `live`. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
|
||||
|
||||
Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent-walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
|
||||
Bundle names resolve from the dsh installation first, then from the profile directory. In-box bundles (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, `@deepseek-ai/dsh-sdk-app`, `@deepseek-ai/dsh-sdk-minimal`, `@deepseek-ai/dsh-acp-app`) therefore always come from the same installation as the running `dsh`; out-of-tree bundles come from the profile's pnpm-managed `node_modules`. A bare plugin `name` in any patch row resolves through the profile directory's Node parent walk, which reaches the maintained installation fallback `$DSH_HOME/profiles/node_modules`. Plain Node installations place one healed symlink there per dependency-closure package. A pkg executable instead places a real ESM proxy that mirrors explicit exports and re-exports the virtual package URL, because operating-system symlinks cannot enter pkg's `/snapshot` filesystem.
|
||||
|
||||
The `web`, `headless`, `sdk`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
|
||||
The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize from shipped templates on first use (`web`: base + web-app with live patches; `headless`: base + headless with startup-only patches; `sdk`: base + sdk-app with startup-only patches; `sdk-minimal`: its standalone bundle with startup-only patches; `acp`: base + acp-app with startup-only patches). Any other missing profile fails loud with a hint to run `dsh plugin --profile <name> add <package>`.
|
||||
|
||||
### App arguments
|
||||
|
||||
@@ -27,6 +27,7 @@ The shipped apps own these command lines:
|
||||
| `web` | `--host`, `--port`, repeatable `--trusted-host`, `--no-open` |
|
||||
| `headless` | the task text, as the positional argument |
|
||||
| `sdk` | no options; stdio carries the JSON-RPC protocol |
|
||||
| `sdk-minimal` | no options; stdio carries the same JSON-RPC protocol |
|
||||
| `acp` | no options; stdio carries Agent Client Protocol |
|
||||
|
||||
A one-shot task (`dsh --profile headless "run the tests"`) creates one fresh persisted Agent through the core registry, submits the task, waits for quiescence, and flushes the Session before deriving the last non-empty assistant text and final `turn/end` reason from its durable interval. It prints the text on stdout and exits 0 for `completed`, else 1. An invocation with no task is a usage error from that app. The shipped headless profile mounts no ApiProxy, Host, HTTP server, Web runtime, or browser client; a successful run writes nothing to stderr and opens no listening port.
|
||||
@@ -80,9 +81,9 @@ The production Web runner needs built package and frontend artifacts (`pnpm run
|
||||
|
||||
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
|
||||
|
||||
All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers.
|
||||
The base-backed modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. The standalone `sdk-minimal` profile uses the invoking directory as its local filesystem and sandbox-policy root but intentionally omits instruction discovery and SQLite. A `patchReload: live` profile watches valid edits of both `cordis.patch.yml` layers (profile and home) and reapplies them transactionally; a `startup` profile applies them once. A one-shot surface exits through its bounded shutdown, which disposes any live watchers.
|
||||
|
||||
New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one.
|
||||
New sessions in base-backed profiles default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads and network access are not confined, while process visibility depends on the selected sandbox backend — bwrap runs commands in a private PID namespace that hides host processes, and Landlock and Seatbelt leave host process visibility unchanged. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. The standalone `sdk-minimal` tree instead pins `danger-full-access` and mounts no approval or permission-settings service.
|
||||
|
||||
`DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. The shipped `minimal` agent preset keeps that deployment presentation, fixes the complete system prompt to `You are a helpful software engineer assistant.`, and composes only persistent `bash` plus `str_replace_editor`. Select 极简模式 when creating a Web session; every other prompt section and model-facing plugin remains absent from that agent while the shared browser, workspace, persistence, sandbox, and permission host stays in place.
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
`dsh --profile <name>` 启动位于 `$DSH_HOME/profiles/<name>` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch <path>` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。`dsh.profile.patchReload` 可选择 `live` patch 文件监视或 `startup` 单次加载;自定义 profile 省略该值时默认使用 `live`。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。
|
||||
|
||||
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。
|
||||
组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`、`@deepseek-ai/dsh-sdk-app`、`@deepseek-ai/dsh-sdk-minimal`、`@deepseek-ai/dsh-acp-app`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。普通 Node 安装会为依赖闭包中的每个包放置并修复一个符号链接。pkg 可执行程序则放置真实 ESM 代理,镜像显式 exports 并重新导出虚拟包 URL,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统。
|
||||
|
||||
`web`、`headless`、`sdk` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
|
||||
`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app,实时应用 patch;`headless`:base + headless,只在启动时应用 patch;`sdk`:base + sdk-app,只在启动时应用 patch;`sdk-minimal`:独立组合包,只在启动时应用 patch;`acp`:base + acp-app,只在启动时应用 patch)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile <name> add <package>`。
|
||||
|
||||
### 应用参数
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| `web` | `--host`、`--port`、可重复的 `--trusted-host`、`--no-open` |
|
||||
| `headless` | 任务文本,作为位置参数 |
|
||||
| `sdk` | 无选项;stdio 携带 JSON-RPC 协议 |
|
||||
| `sdk-minimal` | 无选项;stdio 携带相同的 JSON-RPC 协议 |
|
||||
| `acp` | 无选项;stdio 携带 Agent Client Protocol |
|
||||
|
||||
一次性任务(`dsh --profile headless "run the tests"`)通过核心注册表创建一个全新的持久化 Agent(智能体),提交任务、等待完全停稳并对会话执行 flush,再从其持久化事件区间中推导最后一个非空 assistant 文本与最终 `turn/end` 原因。它在 stdout 打印文本,并在原因为 `completed` 时以 0 退出,否则以 1 退出。没有任务的调用是该应用的用法错误。随附 headless profile 不挂载 ApiProxy、Host、HTTP 服务器、Web 运行时或浏览器客户端;成功运行不会向 stderr 写入任何内容,也不会打开监听端口。
|
||||
@@ -80,9 +81,9 @@ dsh web --help
|
||||
|
||||
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
|
||||
|
||||
所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
|
||||
基于 base 的模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。独立的 `sdk-minimal` profile 把运行命令时所在的目录作为本地文件系统与沙箱策略根目录,但刻意省略指令发现与 SQLite。`patchReload: live` profile 会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;`startup` profile 则只应用一次。一次性运行模式通过有界关闭流程退出,该流程会 dispose(资源释放)所有实时监视器。
|
||||
|
||||
新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。
|
||||
基于 base 的 profile 中,新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取和网络访问不受限制,进程可见性则取决于所选沙箱后端——bwrap 在私有 PID 命名空间中运行命令并隐藏宿主进程,Landlock 与 Seatbelt 保持宿主进程可见性不变。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。独立的 `sdk-minimal` 配置树则固定为 `danger-full-access`,且不挂载 approval 或权限 settings 服务。
|
||||
|
||||
`DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。随附的 `minimal` agent preset 会保留该部署的呈现方式,将完整系统提示词固定为 `You are a helpful software engineer assistant.`,并且仅组合持久 `bash` 和 `str_replace_editor`。创建 Web 会话时请选择极简模式;该 agent 不包含任何其他提示词段落或面向模型的插件,而共享的浏览器、workspace、持久化、沙箱与权限宿主保持不变。
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ switch (invocation.mode) {
|
||||
}
|
||||
case 'dump-config': {
|
||||
const { runDumpConfig } = await import('./dump-config.ts')
|
||||
runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
|
||||
await runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -26,9 +26,10 @@ const NAME = 'dsh'
|
||||
* (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
|
||||
* never parsed).
|
||||
* @param patches - `--patch` overlay paths, in argv order.
|
||||
* @returns settlement after the profile is healed and the dump is written.
|
||||
*/
|
||||
export function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): void {
|
||||
const loaded = prepareProfile(profile, !defaultOnly)
|
||||
export async function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): Promise<void> {
|
||||
const loaded = await prepareProfile(profile, !defaultOnly)
|
||||
const layers: ConfigDumpLayer[] = loaded.layers.map(layer => ({
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
|
||||
@@ -115,8 +115,8 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
|
||||
* @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
|
||||
* @returns the loaded profile.
|
||||
*/
|
||||
export function prepareProfile(name: string, userLayer = true): Profile {
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
export async function prepareProfile(name: string, userLayer = true): Promise<Profile> {
|
||||
await healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
|
||||
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
|
||||
return profile
|
||||
@@ -145,8 +145,8 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
||||
* platform on its own rows), the profile's user layer, the home-level user
|
||||
* `dsh.profile.bundles` order (a base-backed profile gets the base bundle's
|
||||
* platform-gated shell rows), the profile's user layer, the home-level user
|
||||
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
||||
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then the telemetry switch.
|
||||
@@ -154,11 +154,11 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
* @returns the profile and its patch layers.
|
||||
*/
|
||||
function composeProfile(
|
||||
async function composeProfile(
|
||||
name: string,
|
||||
patchFiles: readonly string[],
|
||||
): ComposedProfile {
|
||||
const profile = prepareProfile(name)
|
||||
): Promise<ComposedProfile> {
|
||||
const profile = await prepareProfile(name)
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
@@ -207,7 +207,7 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown
|
||||
* @returns the settled root context and the shutdown controller.
|
||||
*/
|
||||
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
|
||||
const composed = composeProfile(options.profile, options.patchFiles)
|
||||
const composed = await composeProfile(options.profile, options.patchFiles)
|
||||
const app: { current?: Context } = {}
|
||||
const appReady = createAppReady()
|
||||
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
type SessionNotification,
|
||||
} from '@agentclientprotocol/sdk'
|
||||
import { startMockLlmServer } from '@deepseek-ai/dsh-llm-mock-server'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { execa } from 'execa'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
/** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */
|
||||
@@ -924,6 +926,37 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/)
|
||||
}, 30_000)
|
||||
|
||||
it('prints the exact standalone sdk-minimal tree without dsh-base', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin(
|
||||
['--profile', 'sdk-minimal', '--dump-default-config'],
|
||||
{ DSH_HOME: home },
|
||||
)
|
||||
expect(code).toBe(0)
|
||||
expect(stderr).toBe('')
|
||||
const rows = yaml.load(stdout, { schema: entryListSchema }) as Array<{ id?: string; name?: string }>
|
||||
expect(rows.map(row => [row.id, row.name])).toEqual([
|
||||
['sdk-app-startup', '@deepseek-ai/dsh-sdk-app'],
|
||||
['sdk-jsonrpc-server', '@deepseek-ai/dsh-sdk-jsonrpc-server'],
|
||||
['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'],
|
||||
['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'],
|
||||
['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'],
|
||||
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'],
|
||||
['sandbox', '@deepseek-ai/dsh-sandbox-local'],
|
||||
['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'],
|
||||
['subprocess', '@deepseek-ai/dsh-subprocess-local'],
|
||||
['pty', '@deepseek-ai/dsh-terminal'],
|
||||
['terminal-bash', '@deepseek-ai/dsh-terminal-bash'],
|
||||
['fs-local', '@deepseek-ai/dsh-fs-local'],
|
||||
['agent-spine', '@deepseek-ai/dsh-agent-spine-demo'],
|
||||
['persistent-bash', '@deepseek-ai/dsh-tool-bash-persistent'],
|
||||
['str-replace-editor', '@deepseek-ai/dsh-tool-str-replace-editor'],
|
||||
['sessions', '@deepseek-ai/dsh-session-persistence-jsonl'],
|
||||
])
|
||||
expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal')
|
||||
expect(stdout).not.toContain('@deepseek-ai/dsh-base')
|
||||
expect(stdout).not.toContain('@deepseek-ai/dsh-web-app')
|
||||
}, 30_000)
|
||||
|
||||
it('composes the profile user layer and a --patch overlay in order', async () => {
|
||||
// Auto-init the web profile first, then write its user layer.
|
||||
const init = await runBuiltBin(['--profile', 'web', '--dump-default-config'], { DSH_HOME: home })
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
const REPOSITORY_ROOT = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
|
||||
/** Load one shipped bundle patch through the same parser as profile boot. */
|
||||
function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'web-app'): PatchOptions[] {
|
||||
function bundle(name: 'acp-app' | 'base' | 'headless' | 'sdk-app' | 'sdk-minimal' | 'web-app'): PatchOptions[] {
|
||||
return loadOverlayPatches('profile-hmr test', join(REPOSITORY_ROOT, 'packages', 'bundle', name, 'cordis.patch.yml'))
|
||||
}
|
||||
|
||||
@@ -39,4 +39,8 @@ describe('profile module-HMR policy', () => {
|
||||
config: { root: ['.'] },
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the standalone sdk-minimal tree free of module HMR', () => {
|
||||
expect(composeEntries([bundle('sdk-minimal')]).find(entry => entry.id === 'hmr')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,7 +107,7 @@ async function bootWeb(
|
||||
// upward walk. The flat fallback the preset boot maintains is what makes
|
||||
// them resolvable — the same mechanism, not a test-only shim.
|
||||
const home = dirname(settingsFile)
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
||||
await healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
||||
const profileDir = join(home, 'profiles', 'spec')
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
// Product Bundles are installed into the Profile, not the dsh app. Model
|
||||
|
||||
@@ -519,7 +519,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// The production module-resolution setup: an empty profile root inside the temp
|
||||
// harness home, with bare plugin names resolving through the flat module
|
||||
// fallback the launcher heals under <home>/profiles.
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
|
||||
await healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
|
||||
const profileDir = join(harnessHome, 'profiles', 'scaffold')
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
|
||||
@@ -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 docs/architecture.md
|
||||
architecture.md: e37f2321377242803fb89f0a2258682e6c52801c
|
||||
architecture.zh.md: 69abbcdf52654ecbce6549d25ee089b937228123
|
||||
architecture.md: add615c252948adab8db7dec7059f94b8f45e52c
|
||||
architecture.zh.md: 48baf14d6a29e5ef77e6e47f4d9fa9adc4e9e748
|
||||
|
||||
@@ -16,17 +16,17 @@ There is no privileged core to patch: you extend dsh by mounting a plugin beside
|
||||
|
||||
A running `dsh` is a plugin tree composed at boot from ordered layers.
|
||||
|
||||
A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, and `acp` ship as templates.
|
||||
A **profile** is a named composition stored in the Harness home. It lists the bundles it stacks, holds any out-of-tree plugins it installs, and keeps the user's own `cordis.patch.yml`. `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` ship as templates.
|
||||
|
||||
A **bundle** is a distribution format for Cordis config rows and the code they mount, so whatever it inserts stays patchable by the layers above it.
|
||||
|
||||
Each declares itself in its own `package.json` under a `dsh` field: `dsh.profile` lists a profile's bundles, and `dsh.bundle` points at a bundle's patch file.
|
||||
|
||||
[`dsh-base`](../packages/bundle/base/README.md) is the first layer of every profile: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server.
|
||||
[`dsh-base`](../packages/bundle/base/README.md) is the shared first layer of the `web`, `headless`, `sdk`, and `acp` profiles: model adapters, tools, persistence, sandbox and approval policy, settings, credentials, telemetry. [`dsh-web-app`](../packages/bundle/web-app/README.md) adds the browser application, [`dsh-headless`](../packages/bundle/headless/README.md) adds a one-shot runner with no server, [`dsh-sdk-app`](../packages/bundle/sdk-app/README.md) adds the SDK JSON-RPC server, and [`dsh-acp-app`](../packages/bundle/acp-app/README.md) adds the automation-only ACP server. [`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.md) is the deliberate exception: one bundle owns its complete explicit SDK tree and does not apply `dsh-base`.
|
||||
|
||||
Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's `cordis.patch.yml`, then the home-level one, then any `--patch` overlay. A patch targets a row by id and replaces its whole config, or inserts new rows.
|
||||
|
||||
Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle.
|
||||
Custom profiles default to live patch reload. The shipped `web` profile is live; `headless`, `sdk`, `sdk-minimal`, and `acp` apply all layers once at startup because replacing a one-shot or stdio application's dependencies after it owns work would invalidate that lifecycle.
|
||||
|
||||
To see the tree your machine actually boots:
|
||||
|
||||
@@ -40,11 +40,11 @@ Composition mechanics are in [app-boot](../packages/boot/app-boot/README.md#prof
|
||||
|
||||
## Application launch
|
||||
|
||||
Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree.
|
||||
Every supported Node application starts at the `dsh` CLI with a named profile. The shipped applications are `dsh web` (the deliberate alias for `--profile web`), `dsh --profile headless`, `dsh --profile sdk`, `dsh --profile sdk-minimal`, and `dsh --profile acp`. The TypeScript SDK resolves its same-version `dsh` dependency and selects `sdk`; custom plugin composition remains a profile plus ordered patch files, not another executable or inline application tree. `sdk-minimal` is a repository-owned standalone bundle behind the same launcher, not a caller-supplied Cordis tree.
|
||||
|
||||
Vendored CLIs, build-only and test-only executables, direct in-process plugin mounting, and the private browser WebWorker preview are not Harness application launchers. [`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts) keeps every package bin, executable source, and root demo in an explicit class and rejects a Node application path that bypasses `dsh`.
|
||||
|
||||
The packaged Python SDK runtime is the sole temporary application exception. Its private [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.md) carrier and `dsh-sdk-python-runtime-closure` deploy manifest preserve the current Python API, wire, default `cordis.yml`, environment variables, wheel names, `dsh-jsonrpc-agent-pkg-<platform>-<arch>` executables, sidecars, and platform set. A later Python migration will launch `dsh --profile sdk`, delete the private direct-config carrier, and then rename that executable family to `deepseek-harness-sdk-runtime-<platform>-<arch>`.
|
||||
The Python SDK follows the same application architecture. Its runtime wheel packages the normal `dsh` CLI as `deepseek-harness-sdk-runtime-<platform>-<arch>`, and the client launches `dsh --profile sdk` with an explicit Harness home by default. The minimal example selects the shipped `sdk-minimal` profile. Python exposes profile selection and ordered patch files rather than a complete Cordis tree; persistent external plugins are installed through `dsh plugin`. The removed private direct-config carrier has no compatibility bin or fallback parser.
|
||||
|
||||
## Core packages
|
||||
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
|
||||
运行中的 `dsh` 是一棵插件树,由启动时按序叠加的各层组合而成。
|
||||
|
||||
**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk` 和 `acp` 作为模板随发行版交付。
|
||||
**profile** 是存放在 Harness home 中的具名组装。它列出自己叠放的组合包,存放自己安装的树外插件,并保存用户自己的 `cordis.patch.yml`。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` 作为模板随发行版交付。
|
||||
|
||||
**组合包**是 Cordis 配置项及其挂载代码的分发格式,因此它插入的内容始终可被其上各层 patch。
|
||||
|
||||
两者都在各自的 `package.json` 中通过 `dsh` 字段声明自己:`dsh.profile` 列出一个 profile 的组合包,`dsh.bundle` 指向一个组合包的 patch 文件。
|
||||
|
||||
[`dsh-base`](../packages/bundle/base/README.zh.md) 是每个 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。
|
||||
[`dsh-base`](../packages/bundle/base/README.zh.md) 是 `web`、`headless`、`sdk` 与 `acp` profile 的共享第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。[`dsh-web-app`](../packages/bundle/web-app/README.zh.md) 增加浏览器应用,[`dsh-headless`](../packages/bundle/headless/README.zh.md) 增加不带服务器的一次性运行器,[`dsh-sdk-app`](../packages/bundle/sdk-app/README.zh.md) 增加 SDK JSON-RPC 服务器,[`dsh-acp-app`](../packages/bundle/acp-app/README.zh.md) 增加仅用于自动化的 ACP 服务器。[`dsh-sdk-minimal`](../packages/bundle/sdk-minimal/README.zh.md) 是刻意保留的例外:一个组合包拥有完整的显式 SDK 配置树,不应用 `dsh-base`。
|
||||
|
||||
各层按此顺序应用在空条目列表之上:先按 profile 列出的顺序应用每个组合包,然后是 profile 的 `cordis.patch.yml`,然后是 home 级的那份,最后是任意 `--patch` overlay。一条 patch 按 id 定位某个条目并替换其整个 config,或插入新条目。
|
||||
|
||||
自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。
|
||||
自定义 profile 默认实时重载 patch。随附的 `web` profile 使用实时重载;`headless`、`sdk`、`sdk-minimal` 和 `acp` 则只在启动时应用一次所有配置层,因为一次性应用或 stdio 应用拥有工作之后,替换其依赖会破坏该生命周期。
|
||||
|
||||
要查看你的机器实际启动的配置树:
|
||||
|
||||
@@ -40,11 +40,11 @@ dsh --profile web --dump-config
|
||||
|
||||
## 应用启动
|
||||
|
||||
所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。
|
||||
所有受支持的 Node 应用都从 `dsh` CLI 与具名 profile 启动。随附应用是 `dsh web`(刻意为 `--profile web` 保留的别名)、`dsh --profile headless`、`dsh --profile sdk`、`dsh --profile sdk-minimal` 与 `dsh --profile acp`。TypeScript SDK 会解析其同版本 `dsh` 依赖并选择 `sdk`;自定义插件组合继续由 profile 与有序 patch 文件表达,而不是另一个可执行文件或内联应用树。`sdk-minimal` 是位于同一 launcher 后的仓库自有独立组合包,而不是由调用方提供的 Cordis 配置树。
|
||||
|
||||
Vendored CLI、仅用于构建和测试的可执行文件、进程内直接挂载插件以及私有浏览器 WebWorker 预览都不属于 Harness 应用启动器。[`verify-application-entrypoints`](../scripts/verify-application-entrypoints.ts)将每个包 bin、可执行源码与根 demo 归入显式类别,并拒绝任何绕过 `dsh` 的 Node 应用路径。
|
||||
|
||||
打包后的 Python SDK 运行时是唯一的临时应用例外。其私有 [`dsh-sdk-python-runtime`](../packages/sdk/python-runtime/README.zh.md) 载体与 `dsh-sdk-python-runtime-closure` 部署 manifest 保持当前 Python API、协议格式、默认 `cordis.yml`、环境变量、wheel 包名称、`dsh-jsonrpc-agent-pkg-<platform>-<arch>` 可执行文件、伴随文件及平台集合不变。后续 Python 迁移会改为启动 `dsh --profile sdk`、删除私有直读配置载体,然后把该可执行文件族重命名为 `deepseek-harness-sdk-runtime-<platform>-<arch>`。
|
||||
Python SDK 遵循相同的应用架构。其运行时 wheel 把普通 `dsh` CLI 打包为 `deepseek-harness-sdk-runtime-<platform>-<arch>`,客户端默认以显式 Harness home 启动 `dsh --profile sdk`。极简示例选择随附的 `sdk-minimal` profile。Python 暴露 profile 选择与有序 patch 文件,而不是完整 Cordis 树;持久外部插件通过 `dsh plugin` 安装。已删除的私有直读配置载体没有兼容 bin 或回退 parser。
|
||||
|
||||
## 核心包
|
||||
|
||||
|
||||
@@ -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 docs/config-catalog.md
|
||||
config-catalog.md: 46228907ea4241e69a4439c2dc1c1786de94eaff
|
||||
config-catalog.zh.md: ed09a3d44f93815ab667ef4613249a33306d9899
|
||||
config-catalog.md: 7f7e5d3c58953ea50c43eed0d903f0d5469e7849
|
||||
config-catalog.zh.md: f9ab7a8537e29cb0e74e05e74b4a7890d146c28b
|
||||
|
||||
+33
-6
@@ -166,9 +166,10 @@ Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/ag
|
||||
* Bundle config: each field forwarded verbatim to the child that owns it —
|
||||
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
|
||||
* bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`,
|
||||
* `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener,
|
||||
* dynamic-context policy, deployment persona, and explicit model-facing tool
|
||||
* order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin
|
||||
* (the fixed opener, dynamic-context policy, deployment persona completeness,
|
||||
* and explicit model-facing tool order), the `tools` object to the tool
|
||||
* registry (its presentation `mode`),
|
||||
* `dshHome` to bash environment and local skill discovery, `sessionTitle` to
|
||||
* the fallback title service, `skills` to the
|
||||
* skill registry/local provider/tool consumer, `workspaceContext` to the
|
||||
@@ -196,6 +197,8 @@ export interface Config {
|
||||
includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext']
|
||||
/** The deployment persona (see dsh-system-prompt's `Config`). */
|
||||
persona?: SystemPromptConfig['persona']
|
||||
/** Whether the deployment persona is the complete system prompt. */
|
||||
personaComplete?: SystemPromptConfig['personaComplete']
|
||||
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
|
||||
toolOrder?: SystemPromptConfig['toolOrder']
|
||||
/** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */
|
||||
@@ -247,7 +250,7 @@ export interface GoalConfig {
|
||||
|
||||
Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts)
|
||||
|
||||
Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
|
||||
Source: [`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-agent-tool-presentation"></a>
|
||||
|
||||
@@ -1678,6 +1681,22 @@ Depends on: [`SandboxMode`](subsystems/sandbox.md)
|
||||
|
||||
Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-sdk-app"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-sdk-app`
|
||||
|
||||
Requires: `cmdlineArgs`
|
||||
|
||||
```ts config-catalog
|
||||
/** SDK stdio startup configuration. */
|
||||
export interface Config {
|
||||
/** Profile name rendered in help and diagnostics (default `sdk`). */
|
||||
profile?: string
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-sdk-jsonrpc-server"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
|
||||
@@ -1689,6 +1708,13 @@ Requires: `agents`
|
||||
export interface JsonRpcConfig {
|
||||
/** Report max-token turn/subagent termination as a successful SDK result. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
|
||||
toolFilter?: {
|
||||
/** Global tool names that remain visible. */
|
||||
allow?: string[]
|
||||
/** Global tool names removed from visibility. */
|
||||
deny?: string[]
|
||||
}
|
||||
/** Transport input override; production uses `process.stdin`. */
|
||||
input?: Readable
|
||||
/** Transport output override; production uses `process.stdout`. */
|
||||
@@ -2414,6 +2440,8 @@ export interface Config {
|
||||
* `deployment:persona` shadows it; `{{variable}}` references are strict.
|
||||
*/
|
||||
persona?: string
|
||||
/** Treat the deployment persona as the complete system prompt (default false). */
|
||||
personaComplete?: boolean
|
||||
/**
|
||||
* Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
|
||||
* Invalid fields fail at load and unknown names fail at assembly; known names
|
||||
@@ -3318,7 +3346,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
- `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
|
||||
- `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts))
|
||||
- `@deepseek-ai/dsh-schedule` — requires `agents` · `sessions` · `tools` · `sessionPersistence` ([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-app` — requires `cmdlineArgs` ([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-log-export` — requires `commands` ([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts))
|
||||
@@ -3388,8 +3415,8 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||
- `@deepseek-ai/dsh-sandbox-windows-acl` ([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
|
||||
- `@deepseek-ai/dsh-scope` ([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-client` ([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-minimal` ([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-protocol` ([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-python-runtime` ([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-telemetry` ([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-title-llm` ([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
|
||||
- `@deepseek-ai/dsh-subagent-in-process-driver` ([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts))
|
||||
|
||||
@@ -168,9 +168,10 @@ export type PresetTrust = 'system' | 'user'
|
||||
* Bundle config: each field forwarded verbatim to the child that owns it —
|
||||
* `agents` to the agent loop (an app that pre-creates no agents, like the ACP
|
||||
* bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`,
|
||||
* `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener,
|
||||
* dynamic-context policy, deployment persona, and explicit model-facing tool
|
||||
* order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* `persona`, `personaComplete`, and `toolOrder` to the system-prompt plugin
|
||||
* (the fixed opener, dynamic-context policy, deployment persona completeness,
|
||||
* and explicit model-facing tool order), the `tools` object to the tool
|
||||
* registry (its presentation `mode`),
|
||||
* `dshHome` to bash environment and local skill discovery, `sessionTitle` to
|
||||
* the fallback title service, `skills` to the
|
||||
* skill registry/local provider/tool consumer, `workspaceContext` to the
|
||||
@@ -198,6 +199,8 @@ export interface Config {
|
||||
includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext']
|
||||
/** The deployment persona (see dsh-system-prompt's `Config`). */
|
||||
persona?: SystemPromptConfig['persona']
|
||||
/** Whether the deployment persona is the complete system prompt. */
|
||||
personaComplete?: SystemPromptConfig['personaComplete']
|
||||
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
|
||||
toolOrder?: SystemPromptConfig['toolOrder']
|
||||
/** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */
|
||||
@@ -249,7 +252,7 @@ export interface GoalConfig {
|
||||
|
||||
依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`JobsConfig`](#deepseek-aidsh-jobs-local) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillFileSystem`](../packages/skill/skill-filesystem/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/shell/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`toolJobs`](../packages/jobs/tool-jobs/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`workspaceContext`](../packages/context/agent-instructions/src/index.ts)
|
||||
|
||||
来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
|
||||
来源:[`packages/examples/agent-spine-demo/src/index.ts:93`](../packages/examples/agent-spine-demo/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-agent-tool-presentation"></a>
|
||||
|
||||
@@ -1680,6 +1683,22 @@ export interface Config {
|
||||
|
||||
来源:[`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-sdk-app"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-sdk-app`
|
||||
|
||||
需要:`cmdlineArgs`
|
||||
|
||||
```ts config-catalog
|
||||
/** SDK stdio startup configuration. */
|
||||
export interface Config {
|
||||
/** Profile name rendered in help and diagnostics (default `sdk`). */
|
||||
profile?: string
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-sdk-jsonrpc-server"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
|
||||
@@ -1691,6 +1710,13 @@ export interface Config {
|
||||
export interface JsonRpcConfig {
|
||||
/** Report max-token turn/subagent termination as a successful SDK result. */
|
||||
maxTokensAsSuccess?: boolean
|
||||
/** Per-root-agent model-facing tool filter; an allow list excludes later unnamed global tools. */
|
||||
toolFilter?: {
|
||||
/** Global tool names that remain visible. */
|
||||
allow?: string[]
|
||||
/** Global tool names removed from visibility. */
|
||||
deny?: string[]
|
||||
}
|
||||
/** Transport input override; production uses `process.stdin`. */
|
||||
input?: Readable
|
||||
/** Transport output override; production uses `process.stdout`. */
|
||||
@@ -1702,7 +1728,7 @@ export interface JsonRpcConfig {
|
||||
|
||||
依赖:`Readable`(`node:stream`)· `Writable`(`node:stream`)
|
||||
|
||||
来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
|
||||
来源:[`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-session-log-deepseek"></a>
|
||||
|
||||
@@ -2416,6 +2442,8 @@ export interface Config {
|
||||
* `deployment:persona` shadows it; `{{variable}}` references are strict.
|
||||
*/
|
||||
persona?: string
|
||||
/** Treat the deployment persona as the complete system prompt (default false). */
|
||||
personaComplete?: boolean
|
||||
/**
|
||||
* Model-facing tool names in order, with {@link TOOL_ORDER_REST} exactly once.
|
||||
* Invalid fields fail at load and unknown names fail at assembly; known names
|
||||
@@ -3320,7 +3348,6 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-llm`([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
|
||||
- `@deepseek-ai/dsh-lsp`([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts))
|
||||
- `@deepseek-ai/dsh-schedule` — 需要 `agents` · `sessions` · `tools` · `sessionPersistence`([`packages/schedule/schedule/src/index.ts`](../packages/schedule/schedule/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-app` — 需要 `cmdlineArgs`([`packages/bundle/sdk-app/src/index.ts`](../packages/bundle/sdk-app/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session`([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-checkpoint-policy` — 需要 `llm` · `sessionPersistence` · `sessions` · `tools`([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-log-export` — 需要 `commands`([`packages/session-query/session-log-export/src/index.ts`](../packages/session-query/session-log-export/src/index.ts))
|
||||
@@ -3389,8 +3416,8 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
|
||||
- `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-client`([`packages/sdk/client/src/index.ts`](../packages/sdk/client/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-minimal`([`packages/bundle/sdk-minimal/src/index.ts`](../packages/bundle/sdk-minimal/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-protocol`([`packages/sdk/protocol/src/index.ts`](../packages/sdk/protocol/src/index.ts))
|
||||
- `@deepseek-ai/dsh-sdk-python-runtime`([`packages/sdk/python-runtime/src/index.ts`](../packages/sdk/python-runtime/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-telemetry`([`packages/session/session-telemetry/src/index.ts`](../packages/session/session-telemetry/src/index.ts))
|
||||
- `@deepseek-ai/dsh-session-title-llm`([`packages/session/session-title-llm/src/index.ts`](../packages/session/session-title-llm/src/index.ts))
|
||||
- `@deepseek-ai/dsh-subagent-in-process-driver`([`packages/subagent/subagent-in-process-driver/src/index.ts`](../packages/subagent/subagent-in-process-driver/src/index.ts))
|
||||
|
||||
@@ -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 docs/module-graph.md
|
||||
module-graph.md: aeb35195f3a095b9de694f164dc1111acf5c8197
|
||||
module-graph.zh.md: ef3bc3fbce9cad37542eeea6adf936dd70e6581b
|
||||
module-graph.md: a2497ed002c6eec55ec35e1d7e9953feb2ed0ee5
|
||||
module-graph.zh.md: 230d67f4c07f9715d4b5157c1539edd09f9221c3
|
||||
|
||||
@@ -123,6 +123,7 @@ flowchart TD
|
||||
pkg_base["base"]
|
||||
pkg_headless["headless"]
|
||||
pkg_sdk_app["sdk-app"]
|
||||
pkg_sdk_minimal["sdk-minimal"]
|
||||
pkg_web_app["web-app"]
|
||||
end
|
||||
subgraph group_client["packages/client"]
|
||||
@@ -275,7 +276,6 @@ flowchart TD
|
||||
pkg_sdk_client["sdk-client"]
|
||||
pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"]
|
||||
pkg_sdk_protocol["sdk-protocol"]
|
||||
pkg_sdk_python_runtime["sdk-python-runtime"]
|
||||
end
|
||||
subgraph group_session["packages/session"]
|
||||
pkg_session_checkpoint_policy["session-checkpoint-policy"]
|
||||
@@ -367,6 +367,7 @@ flowchart TD
|
||||
pkg_acp_app --> pkg_invariants
|
||||
pkg_base --> pkg_invariants
|
||||
pkg_sdk_app --> pkg_invariants
|
||||
pkg_sdk_minimal --> pkg_invariants
|
||||
pkg_client_store --> pkg_invariants
|
||||
pkg_client_ui_primitives --> pkg_invariants
|
||||
pkg_client_ui_renderer --> pkg_invariants
|
||||
@@ -381,7 +382,6 @@ flowchart TD
|
||||
pkg_host_directory_picker_native --> pkg_invariants
|
||||
pkg_host_webserver --> pkg_invariants
|
||||
pkg_sandbox_windows_acl --> pkg_invariants
|
||||
pkg_sdk_python_runtime --> pkg_invariants
|
||||
pkg_storage --> pkg_invariants
|
||||
pkg_subprocess --> pkg_invariants
|
||||
pkg_win32_process --> pkg_invariants
|
||||
@@ -1658,6 +1658,7 @@ flowchart TD
|
||||
| [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1672,7 +1673,6 @@ flowchart TD
|
||||
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
@@ -125,6 +125,7 @@ flowchart TD
|
||||
pkg_base["base"]
|
||||
pkg_headless["headless"]
|
||||
pkg_sdk_app["sdk-app"]
|
||||
pkg_sdk_minimal["sdk-minimal"]
|
||||
pkg_web_app["web-app"]
|
||||
end
|
||||
subgraph group_client["packages/client"]
|
||||
@@ -277,7 +278,6 @@ flowchart TD
|
||||
pkg_sdk_client["sdk-client"]
|
||||
pkg_sdk_jsonrpc_server["sdk-jsonrpc-server"]
|
||||
pkg_sdk_protocol["sdk-protocol"]
|
||||
pkg_sdk_python_runtime["sdk-python-runtime"]
|
||||
end
|
||||
subgraph group_session["packages/session"]
|
||||
pkg_session_checkpoint_policy["session-checkpoint-policy"]
|
||||
@@ -369,6 +369,7 @@ flowchart TD
|
||||
pkg_acp_app --> pkg_invariants
|
||||
pkg_base --> pkg_invariants
|
||||
pkg_sdk_app --> pkg_invariants
|
||||
pkg_sdk_minimal --> pkg_invariants
|
||||
pkg_client_store --> pkg_invariants
|
||||
pkg_client_ui_primitives --> pkg_invariants
|
||||
pkg_client_ui_renderer --> pkg_invariants
|
||||
@@ -383,7 +384,6 @@ flowchart TD
|
||||
pkg_host_directory_picker_native --> pkg_invariants
|
||||
pkg_host_webserver --> pkg_invariants
|
||||
pkg_sandbox_windows_acl --> pkg_invariants
|
||||
pkg_sdk_python_runtime --> pkg_invariants
|
||||
pkg_storage --> pkg_invariants
|
||||
pkg_subprocess --> pkg_invariants
|
||||
pkg_win32_process --> pkg_invariants
|
||||
@@ -1660,6 +1660,7 @@ flowchart TD
|
||||
| [`acp-app`](../packages/bundle/acp-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-minimal`](../packages/bundle/sdk-minimal) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-store`](../packages/client/store) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
@@ -1674,7 +1675,6 @@ flowchart TD
|
||||
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`host-webserver`](../packages/host/webserver) | `host` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`sdk-python-runtime`](../packages/sdk/python-runtime) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`storage`](../packages/storage/storage) | `storage` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`subprocess`](../packages/subprocess/subprocess) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
| [`win32-process`](../packages/subprocess/win32-process) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants) |
|
||||
|
||||
@@ -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 docs/user/guide/python-sdk.md
|
||||
python-sdk.md: 21c7a908a8524b16baf8f98453746f59b5d5efc8
|
||||
python-sdk.zh.md: bb883f6f3799225bda590a80883063ecb29e15b4
|
||||
python-sdk.md: 24f5594a20eab6870d9725e0f1acfce5dff62f74
|
||||
python-sdk.zh.md: 47b420ab20df04d28e5498807dc73a425c7a9666
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](python-sdk.zh.md)
|
||||
|
||||
This tutorial is the programmatic alternative to the Web UI. It installs the published Python SDK, runs a checked-in agent composition, and shows how to call the same API from your own program.
|
||||
This tutorial installs the published Python SDK, runs the shipped standalone minimal profile, and shows how to customize the same `dsh` profile from your own program.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -10,12 +10,10 @@ This tutorial is the programmatic alternative to the Web UI. It installs the pub
|
||||
- Git
|
||||
- Linux x64, Linux arm64, or macOS 14 or newer on arm64
|
||||
- A DeepSeek-compatible API endpoint and credential
|
||||
- An isolated workspace that the agent may modify
|
||||
- An isolated workspace and an isolated Harness home
|
||||
|
||||
## Install the SDK
|
||||
|
||||
Clone the repository for its runnable example, create a virtual environment, and install the SDK with its same-version bundled runtime:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/deepseek-ai/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
@@ -24,51 +22,45 @@ python -m venv .venv
|
||||
python -m pip install deepseek-harness-sdk
|
||||
```
|
||||
|
||||
The installed runtime needs no system Node.js. Repository contributors who need to build the runtime or wheels from source should use the [Python contributor workflows](../../../python/development.md).
|
||||
The installation includes a matching native runtime wheel and the `dsh` command. Normal SDK execution needs no system Node.js. Repository contributors who build the artifacts should use the [Python contributor workflow](../../../python/development.md).
|
||||
|
||||
## Run the checked-in example
|
||||
|
||||
Set the credential in the environment. Set `DEEPSEEK_BASE_URL` as well when the model is served by an OpenAI-compatible proxy rather than the default DeepSeek endpoint.
|
||||
Export the credential and, when needed, a compatible proxy endpoint:
|
||||
|
||||
```sh
|
||||
export DEEPSEEK_API_KEY=sk-your-key-here
|
||||
# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
|
||||
# export DSH_MODEL=deepseek-v4-flash
|
||||
# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.'
|
||||
```
|
||||
|
||||
Run one task against an isolated workspace and session directory:
|
||||
Run one task with explicit workspace and home paths:
|
||||
|
||||
```sh
|
||||
python examples/python-sdk-agent/minimal.py \
|
||||
--workspace /absolute/path/to/workspace \
|
||||
--session-root /absolute/path/to/sessions \
|
||||
--workspace /absolute/path/to/disposable-workspace \
|
||||
--dsh-home /absolute/path/to/example-dsh-home \
|
||||
--session-id example-001 \
|
||||
"Inspect the repository and fix the failing tests."
|
||||
```
|
||||
|
||||
The script prints the final assistant response. The session directory receives a JSONL log containing the assembled model requests and tool calls.
|
||||
The script prints the final assistant response. The selected home receives the generated `sdk-minimal` profile, installed plugins, and uncompressed JSONL session logs under `sessions/`. The example and SDK never silently read `~/.dsh`.
|
||||
|
||||
## Use the SDK in your own program
|
||||
|
||||
The checked-in example is a thin wrapper around this SDK call:
|
||||
## Use the SDK in your program
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve()
|
||||
workspace = Path("/absolute/path/to/workspace").resolve()
|
||||
sessions = Path("/absolute/path/to/sessions").resolve()
|
||||
|
||||
workspace = Path("/absolute/path/to/disposable-workspace").resolve()
|
||||
dsh_home = Path("/absolute/path/to/example-dsh-home").resolve()
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="deepseek-v4-flash",
|
||||
max_tokens=49_152,
|
||||
cwd=str(workspace),
|
||||
session_root=str(sessions),
|
||||
cordis=str(config),
|
||||
dsh_home=str(dsh_home),
|
||||
profile="sdk-minimal",
|
||||
) as harness:
|
||||
result = harness.run(
|
||||
"Inspect the repository and fix the failing tests.",
|
||||
@@ -78,9 +70,23 @@ with DeepSeekHarness(
|
||||
print(result.final_response)
|
||||
```
|
||||
|
||||
`DeepSeekHarness` starts the bundled runtime lazily and reuses it until the context manager exits. Reusing the same harness and session id preserves the session-owned Bash process, including its working directory, exported variables, and shell functions. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same durable conversation.
|
||||
The SDK starts the bundled `dsh --profile sdk-minimal` process lazily and reuses it until context-manager exit. The profile, its persistent patch, the home patch, and any ordered `patches` tuple form the application configuration. There is no separate Python runtime bin or complete-config option.
|
||||
|
||||
## Understand the example composition
|
||||
## Install or define plugins
|
||||
|
||||
Use `dsh plugin` for dependencies and bundle layers that should persist in this home:
|
||||
|
||||
```sh
|
||||
export DSH_HOME=/absolute/path/to/example-dsh-home
|
||||
dsh --profile sdk-minimal --dump-default-config >/dev/null
|
||||
dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
|
||||
```
|
||||
|
||||
The first command initializes the shipped standalone profile. The second forwards package management to `pnpm`, then records any installed package that exports a `dsh.bundle` layer. Install `pnpm` only for this management command; launching the installed SDK does not need it. Edit `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml` for persistent row changes, or pass patch files from Python for per-launch changes.
|
||||
|
||||
Another `profile` is valid when it includes `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. Missing server rows, unresolved plugins, and invalid patches fail during startup instead of falling back to another composition.
|
||||
|
||||
## Understand the minimal profile
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
@@ -89,16 +95,13 @@ print(result.final_response)
|
||||
| Model-facing tools | Persistent `bash` and `str_replace_editor` only |
|
||||
| Bash timeout | 300 seconds |
|
||||
| Editor output limit | 16,000 characters |
|
||||
| Context compaction | Disabled |
|
||||
| Filesystem | Bare local backend; absolute editor paths may address any path visible to the runtime process |
|
||||
| Session persistence | Uncompressed JSONL under `DSH_SESSION_ROOT` |
|
||||
| Runtime context and compaction | Absent |
|
||||
| Session persistence | Uncompressed JSONL under `<dsh_home>/sessions` |
|
||||
|
||||
The composition omits harness identity, workspace prompt text, skills, one-shot Bash, task tools, compaction, and every other model-facing plugin. Sandbox-policy facts are logged as runtime user context rather than appended to the system prompt.
|
||||
The profile's sole bundle inserts the complete tree over an empty root and does not include `dsh-base`; later base-profile tools therefore cannot appear implicitly. It contains the SDK protocol, one environment-configured DeepSeek adapter, local execution, and persistence, while settings, managed credentials, telemetry, Web tools, subagents, local instruction discovery, and compaction are absent. It pins `danger-full-access`, so persistent Bash and the editor can modify any path visible to the runtime; use a disposable checkout or container. The PTY implementation makes this example POSIX-only.
|
||||
|
||||
## Choose workspace and session IDs
|
||||
The installed wheel still packages the full `web` profile and frontend assets. Run `dsh web` against an explicit `DSH_HOME` when a Python SDK deployment also needs the browser application; `web` is a separate CLI application and cannot serve a Python SDK client.
|
||||
|
||||
`cwd` selects the workspace available to the agent, while `session_root` stores session logs and state. Use a fresh session id for an independent task; reuse an id only when the next call should continue the same conversation and persistent shell state.
|
||||
Use a fresh home when profiles, plugins, credentials, settings, and sessions must be isolated. Use a fresh session id for independent work; reuse a harness, home, and id only to continue the same durable conversation and session-owned resources.
|
||||
|
||||
The composition uses `danger-full-access`. Run it only inside a disposable checkout or container: Bash and the editor can modify any path allowed to the runtime process. The persistent PTY backend requires a POSIX terminal substrate, so this composition does not support Windows agents.
|
||||
|
||||
The [`python-sdk-agent` example reference](../../../examples/python-sdk-agent/README.md) owns the exact composition. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, runtime selection, and configuration; the [Cordis primer](../../cordis-primer.md) covers composition syntax.
|
||||
The [bundle reference](../../../packages/bundle/sdk-minimal/README.md) owns the exact tree, and the [example reference](../../../examples/python-sdk-agent/README.md) owns the runnable program. The [Python SDK reference](../../../python/sdk/README.md) covers lifecycle, results, notifications, and low-level behavior; the [dsh CLI reference](../../../apps/cli/reference/README.md) covers profile layering.
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
# Python SDK 快速上手
|
||||
# Python SDK 入门
|
||||
|
||||
[English](python-sdk.md) | 中文
|
||||
|
||||
本教程介绍 Web UI 之外的程序化使用方式:安装已发布的 Python SDK、运行仓库内置的 agent(智能体)组合,并在自己的程序中调用同一套 API。
|
||||
本教程安装已发布的 Python SDK,运行随附的独立极简 profile,并说明如何从自己的程序自定义同一个 `dsh` profile。
|
||||
|
||||
## 前置要求
|
||||
## 前置条件
|
||||
|
||||
- Python 3.10 或更高版本
|
||||
- Git
|
||||
- Linux x64、Linux arm64 或 macOS 14 或更高版本的 arm64
|
||||
- DeepSeek 兼容的 API 端点与凭据
|
||||
- agent 可以修改的隔离 workspace
|
||||
- Linux x64、Linux arm64,或 arm64 上的 macOS 14 或更高版本
|
||||
- DeepSeek 兼容的 API endpoint 与凭据
|
||||
- 隔离的 workspace 与隔离的 Harness home
|
||||
|
||||
## 安装 SDK
|
||||
|
||||
克隆仓库以使用其中的可运行示例,创建虚拟环境,并安装 SDK 及其同版本内置运行时:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/deepseek-ai/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
@@ -24,51 +22,45 @@ python -m venv .venv
|
||||
python -m pip install deepseek-harness-sdk
|
||||
```
|
||||
|
||||
安装后的运行时不需要系统提供 Node.js。需要从源码构建运行时或 wheel 包的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。
|
||||
安装内容包含匹配的原生运行时 wheel 与 `dsh` 命令。普通 SDK 运行不需要系统 Node.js。需要构建产物的仓库贡献者应使用 [Python 贡献者工作流](../../../python/development.zh.md)。
|
||||
|
||||
## 运行仓库内置示例
|
||||
## 运行检入示例
|
||||
|
||||
请在环境中设置凭据。如果模型不是由默认 DeepSeek 端点提供,而是通过 OpenAI 兼容代理提供,还需要设置 `DEEPSEEK_BASE_URL`。
|
||||
导出凭据;使用兼容代理时再设置 endpoint:
|
||||
|
||||
```sh
|
||||
export DEEPSEEK_API_KEY=sk-your-key-here
|
||||
# export DEEPSEEK_BASE_URL=http://127.0.0.1:8000/v1
|
||||
# export DSH_MODEL=deepseek-v4-flash
|
||||
# export DSH_SYSTEM_PROMPT='You are a helpful software engineer assistant.'
|
||||
```
|
||||
|
||||
针对隔离的 workspace 和会话目录运行一个任务:
|
||||
使用显式 workspace 与 home 路径运行一个任务:
|
||||
|
||||
```sh
|
||||
python examples/python-sdk-agent/minimal.py \
|
||||
--workspace /absolute/path/to/workspace \
|
||||
--session-root /absolute/path/to/sessions \
|
||||
--workspace /absolute/path/to/disposable-workspace \
|
||||
--dsh-home /absolute/path/to/example-dsh-home \
|
||||
--session-id example-001 \
|
||||
"Inspect the repository and fix the failing tests."
|
||||
```
|
||||
|
||||
脚本会打印 assistant 的最终回复。会话目录会收到 JSONL 日志,其中包含组装后的模型请求与工具调用。
|
||||
脚本会打印最终 assistant 响应。所选 home 会保存生成的 `sdk-minimal` profile、已安装插件,以及 `sessions/` 下的未压缩 JSONL 会话日志。示例与 SDK 绝不会静默读取 `~/.dsh`。
|
||||
|
||||
## 在自己的程序中使用 SDK
|
||||
|
||||
仓库内置示例是以下 SDK 调用的轻量包装:
|
||||
## 在程序中使用 SDK
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
config = Path("examples/python-sdk-agent/minimal.cordis.yml").resolve()
|
||||
workspace = Path("/absolute/path/to/workspace").resolve()
|
||||
sessions = Path("/absolute/path/to/sessions").resolve()
|
||||
|
||||
workspace = Path("/absolute/path/to/disposable-workspace").resolve()
|
||||
dsh_home = Path("/absolute/path/to/example-dsh-home").resolve()
|
||||
with DeepSeekHarness(
|
||||
provider="deepseek-official",
|
||||
model="deepseek-v4-flash",
|
||||
max_tokens=49_152,
|
||||
cwd=str(workspace),
|
||||
session_root=str(sessions),
|
||||
cordis=str(config),
|
||||
dsh_home=str(dsh_home),
|
||||
profile="sdk-minimal",
|
||||
) as harness:
|
||||
result = harness.run(
|
||||
"Inspect the repository and fix the failing tests.",
|
||||
@@ -78,27 +70,38 @@ with DeepSeekHarness(
|
||||
print(result.final_response)
|
||||
```
|
||||
|
||||
`DeepSeekHarness` 会延迟启动内置运行时,并持续复用,直至退出上下文管理器。复用同一个 harness 与 session id 会保留该会话拥有的 Bash 进程,包括其工作目录、已导出的变量与 shell 函数。独立任务应使用新的 session id;只有下一次调用需要延续同一段持久化对话时,才复用原有 id。
|
||||
SDK 会延迟启动内置的 `dsh --profile sdk-minimal` 进程,并复用到上下文管理器退出。Profile、其持久 patch、home patch 与任何有序 `patches` tuple 共同组成应用配置。不存在独立 Python 运行时 bin 或完整配置选项。
|
||||
|
||||
## 了解示例组合
|
||||
## 安装或定义插件
|
||||
|
||||
需要在该 home 中持久保存依赖与 bundle 层时,使用 `dsh plugin`:
|
||||
|
||||
```sh
|
||||
export DSH_HOME=/absolute/path/to/example-dsh-home
|
||||
dsh --profile sdk-minimal --dump-default-config >/dev/null
|
||||
dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
|
||||
```
|
||||
|
||||
第一个命令初始化随附的独立 profile。第二个命令把包管理转发给 `pnpm`,然后记录所有导出 `dsh.bundle` 层的已安装包。只有执行此管理命令时才需要安装 `pnpm`;启动已安装 SDK 不需要它。持久配置项变更应编辑 `$DSH_HOME/profiles/sdk-minimal/cordis.patch.yml`;单次启动变更则从 Python 传入 patch 文件。
|
||||
|
||||
另一个 `profile` 只有包含 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项时才有效。缺失 server 配置项、无法解析的插件和非法 patch 会在启动时失败,不会回退到其他组合。
|
||||
|
||||
## 理解极简 profile
|
||||
|
||||
| 属性 | 值 |
|
||||
|---|---|
|
||||
| 系统提示词 | `DSH_SYSTEM_PROMPT`;未设置时使用 `You are a helpful software engineer assistant.` |
|
||||
| `minimal.py` 使用的模型 | `--model`,其次为 `DSH_MODEL`,最后为 `deepseek-v4-flash` |
|
||||
| 系统提示词 | `DSH_SYSTEM_PROMPT`,未设置时为 `You are a helpful software engineer assistant.` |
|
||||
| `minimal.py` 的模型 | `--model`,然后是 `DSH_MODEL`,最后是 `deepseek-v4-flash` |
|
||||
| 面向模型的工具 | 仅持久 `bash` 与 `str_replace_editor` |
|
||||
| Bash 超时 | 300 秒 |
|
||||
| 编辑器输出上限 | 16,000 个字符 |
|
||||
| 上下文压缩 | 已关闭 |
|
||||
| 文件系统 | 裸本地后端;编辑器使用绝对路径,可以访问运行时进程可见的任何路径 |
|
||||
| 会话持久化 | `DSH_SESSION_ROOT` 下未压缩的 JSONL |
|
||||
| Editor 输出上限 | 16,000 字符 |
|
||||
| 运行时上下文与 compaction | 不存在 |
|
||||
| 会话持久化 | `<dsh_home>/sessions` 下的未压缩 JSONL |
|
||||
|
||||
该组合省略了 harness 身份、workspace 提示词文本、skill(技能)、一次性 Bash、任务工具、上下文压缩和其他所有面向模型的插件。沙箱策略事实记录为运行时用户上下文,而不会追加到系统提示词中。
|
||||
该 profile 的唯一组合包会在空根之上插入完整配置树,且不包含 `dsh-base`,因此基础 profile 以后新增的工具不会隐式出现。它包含 SDK 协议、一个由环境配置的 DeepSeek 适配器、本地执行与持久化;settings、托管凭据、遥测、Web 工具、subagent、本地指令发现和 compaction 均不存在。它固定使用 `danger-full-access`,因此持久 Bash 与 editor 可以修改运行时可见的任何路径;应使用一次性 checkout 或容器。由于采用 PTY 实现,本示例只支持 POSIX。
|
||||
|
||||
## 选择 workspace 与 session id
|
||||
已安装 wheel 仍会打包完整 `web` profile 与前端产物。如果 Python SDK 部署还需要浏览器应用,请针对显式 `DSH_HOME` 运行 `dsh web`;`web` 是独立 CLI 应用,不能为 Python SDK client 提供服务。
|
||||
|
||||
`cwd` 用于选择 agent 可访问的 workspace,`session_root` 用于保存会话日志和状态。独立任务应使用新的 session id;只有下一次调用需要延续同一段对话和持久 shell 状态时,才复用原有 id。
|
||||
需要隔离 profile、插件、凭据、设置与会话时,应使用新的 home。独立工作应使用新的 session id;只有继续同一段持久对话和会话资源时,才同时复用 harness、home 与 id。
|
||||
|
||||
该组合使用 `danger-full-access`。只能在可丢弃的 checkout 或容器内运行:Bash 与编辑器可以修改运行时进程有权访问的任何路径。持久 PTY 后端需要 POSIX 终端环境,因此该组合不支持 Windows agent。
|
||||
|
||||
准确的组合内容归 [`python-sdk-agent` 示例参考](../../../examples/python-sdk-agent/README.zh.md)所有。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知、运行时选择和配置;[Cordis primer](../../cordis-primer.zh.md)介绍组合语法。
|
||||
[组合包参考](../../../packages/bundle/sdk-minimal/README.zh.md)定义确切配置树,[示例参考](../../../examples/python-sdk-agent/README.zh.md)定义可运行程序。[Python SDK 参考](../../../python/sdk/README.zh.md)介绍生命周期、结果、通知与底层行为;[dsh CLI 参考](../../../apps/cli/reference/README.zh.md)介绍 profile 分层。
|
||||
|
||||
@@ -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 examples/python-sdk-agent/README.md
|
||||
README.md: 279aa5bcf0e988168cc936fbd6d96b69e74a5873
|
||||
README.zh.md: d843d8be719349b24e0369a2177748f2b09e40d6
|
||||
README.md: 46a8dc2384d96db39841c4c1e4cdc88d82ff55eb
|
||||
README.zh.md: 5e6e2f89f27398dd7404894f15391a6a693d689b
|
||||
|
||||
@@ -2,39 +2,43 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The unattended coding-agent composition for the Python SDK's bundled JSON-RPC runtime. It intentionally loads no terminal UI, console logger, approval UI, or user-questions tool because stdout belongs to the SDK protocol and turns are driven by the SDK.
|
||||
Runnable Python SDK example over the sole application launcher, `dsh --profile sdk-minimal`. The Python client owns JSON-RPC stdio; the profile owns the agent composition, persistence, execution policy, and plugins.
|
||||
|
||||
The model-facing tools are:
|
||||
## Run the minimal agent
|
||||
|
||||
- `bash`, foreground only
|
||||
- `read`, `write`, and `edit`
|
||||
- `subagent`, using one foreground in-process spawn provider
|
||||
- `todo_write`
|
||||
Install `deepseek-harness-sdk`, export a model credential, then supply an isolated Harness home and workspace:
|
||||
|
||||
The surrounding runtime also loads JSONL session persistence and automatic context compaction. `maxTokensAsSuccess` keeps a token-limited model turn as an accepted evaluation result while preserving its `max-tokens` reason.
|
||||
```sh
|
||||
export DEEPSEEK_API_KEY=sk-your-key-here
|
||||
python examples/python-sdk-agent/minimal.py \
|
||||
--dsh-home /absolute/path/to/example-dsh-home \
|
||||
--workspace /absolute/path/to/disposable-workspace \
|
||||
--session-id example-001 \
|
||||
"Inspect the repository and fix the failing tests."
|
||||
```
|
||||
|
||||
## Runtime environment
|
||||
Set `DEEPSEEK_BASE_URL` for a compatible proxy, `DSH_MODEL` for the script's default model, or `DSH_SYSTEM_PROMPT` for the deployment persona. `--model` is the single runtime model selection; no matching environment variable is required. `--profile` can select another SDK-serving profile. The selected home stores the generated `sdk-minimal` profile and uncompressed JSONL session logs under `sessions/`; the script never reads `~/.dsh` implicitly.
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `DEEPSEEK_API_KEY` | Credential passed to the OpenAI-compatible host endpoint |
|
||||
| `DEEPSEEK_BASE_URL` | Host endpoint used by `dsh-llm-deepseek` |
|
||||
| `DSH_CWD` | Agent workspace for bash and filesystem tools |
|
||||
| `DSH_CONTEXT_WINDOW` | Context capacity recorded for the `DSH_MODEL` catalog entry in the minimal variant |
|
||||
| `DSH_MAX_TOKENS_AS_SUCCESS` | `true` (default) accepts token-limited results; `false` reports them as errors |
|
||||
| `DSH_MODEL` | Default model used by `minimal.py`; `--model` takes precedence |
|
||||
| `DSH_SESSION_ROOT` | JSONL session directory |
|
||||
| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona |
|
||||
|
||||
Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js.
|
||||
|
||||
## Minimal variant
|
||||
|
||||
[`minimal.cordis.yml`](minimal.cordis.yml) is the complete standalone counterpart of the Web `minimal` preset. `DSH_SYSTEM_PROMPT` selects its system prompt, with `You are a helpful software engineer assistant.` as the fallback. It suppresses every system-prompt runtime-context contribution for fresh sessions and mounts no context-compaction plugin. Its model-facing tools are exactly:
|
||||
The shipped [`@deepseek-ai/dsh-sdk-minimal` bundle](../../packages/bundle/sdk-minimal/README.md) is the complete explicit Cordis tree for this mode. It exposes exactly:
|
||||
|
||||
- owner-scoped persistent `bash`
|
||||
- `str_replace_editor` with `view`, `create`, `str_replace`, and `insert`
|
||||
|
||||
It composes the local PTY, bare `fs-local` backend, danger-full-access policy for persistent Bash, and uncompressed JSONL persistence needed by the bundled runtime. Bash and absolute editor paths can modify any path available to the runtime process, so run this variant only against a disposable checkout or container. The persistent PTY requires a POSIX terminal environment and is not a Windows agent interface.
|
||||
The bundle does not include `dsh-base`, so every additional row is an explicit profile change. Runtime context, local instruction discovery, compaction, settings, managed credentials, telemetry, Web tools, subagents, and the full default tool roster are absent. The tree retains SDK startup and JSON-RPC serving, one environment-configured DeepSeek adapter, local execution, and JSONL persistence.
|
||||
|
||||
[`minimal.py`](minimal.py) runs the composition through the Python SDK and uses `DSH_MODEL` as its default model. The [Python SDK tutorial](../../docs/user/guide/python-sdk.md) covers installation, execution, workspace selection, and session identity; the [SDK reference](../../python/sdk/README.md) owns runtime lifecycle and result semantics.
|
||||
This variant is intentionally POSIX-only. Its persistent PTY and editor can modify any path available to the runtime process, so use a disposable checkout or container.
|
||||
|
||||
## Add plugins
|
||||
|
||||
Use the runtime wheel's `dsh` command against the same explicit home for persistent profile changes:
|
||||
|
||||
```sh
|
||||
export DSH_HOME=/absolute/path/to/example-dsh-home
|
||||
dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
|
||||
```
|
||||
|
||||
Use `sdk-minimal` in that command to extend this example, or `sdk` to extend the full base-backed SDK profile. The Python call can also pass additional absolute patch paths in `patches=(...)`; later files win. A selected profile must retain `@deepseek-ai/dsh-sdk-app` or another JSON-RPC server row. The example accepts no complete Cordis file or arbitrary process argv.
|
||||
|
||||
The same runtime wheel packages the `web` profile and its frontend assets for direct CLI use: `dsh web` starts that separate application. A Python SDK client cannot select `web` because it has no JSON-RPC server row.
|
||||
|
||||
See the [Python SDK tutorial](../../docs/user/guide/python-sdk.md) and [SDK reference](../../python/sdk/README.md).
|
||||
|
||||
@@ -2,39 +2,43 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向 Python SDK 内置 JSON-RPC 运行时的无人值守编码 agent(智能体)组合。它有意不加载终端 UI、控制台日志记录器、批准界面或用户交互工具,因为 stdout 属于 SDK 协议,轮次由 SDK 驱动。
|
||||
基于唯一应用启动器 `dsh --profile sdk-minimal` 的可运行 Python SDK 示例。Python 客户端负责 JSON-RPC stdio;profile 负责 agent 组合、持久化、执行策略与插件。
|
||||
|
||||
面向模型的工具为:
|
||||
## 运行极简 agent
|
||||
|
||||
- `bash`,仅前台
|
||||
- `read`、`write` 和 `edit`
|
||||
- `subagent`,使用一个在进程内以前台方式运行的 spawn 提供方
|
||||
- `todo_write`
|
||||
安装 `deepseek-harness-sdk`、导出模型凭据,然后提供隔离的 Harness home 与 workspace:
|
||||
|
||||
周边运行时还加载 JSONL 会话持久化和自动上下文压缩(context compaction)。`maxTokensAsSuccess` 将受 token 上限限制的模型轮次保留为已接受的评估结果,同时保留其 `max-tokens` 原因。
|
||||
```sh
|
||||
export DEEPSEEK_API_KEY=sk-your-key-here
|
||||
python examples/python-sdk-agent/minimal.py \
|
||||
--dsh-home /absolute/path/to/example-dsh-home \
|
||||
--workspace /absolute/path/to/disposable-workspace \
|
||||
--session-id example-001 \
|
||||
"Inspect the repository and fix the failing tests."
|
||||
```
|
||||
|
||||
## 运行时环境
|
||||
兼容代理使用 `DEEPSEEK_BASE_URL`,脚本默认模型使用 `DSH_MODEL`,deployment persona 使用 `DSH_SYSTEM_PROMPT`。`--model` 是唯一运行时模型选择,不要求匹配的环境变量;`--profile` 可以选择另一个提供 SDK 服务的 profile。所选 home 保存生成的 `sdk-minimal` profile,并在 `sessions/` 下保存未压缩 JSONL 会话日志;脚本绝不会隐式读取 `~/.dsh`。
|
||||
|
||||
| 变量 | 用途 |
|
||||
|---|---|
|
||||
| `DEEPSEEK_API_KEY` | 传给 OpenAI 兼容宿主端点的凭据 |
|
||||
| `DEEPSEEK_BASE_URL` | `dsh-llm-deepseek` 使用的宿主端点 |
|
||||
| `DSH_CWD` | bash 和文件系统工具使用的 agent workspace |
|
||||
| `DSH_CONTEXT_WINDOW` | 极简变体中为 `DSH_MODEL` 目录项记录的上下文容量 |
|
||||
| `DSH_MAX_TOKENS_AS_SUCCESS` | `true`(默认)接受受 token 上限限制的结果;`false` 将其报告为错误 |
|
||||
| `DSH_MODEL` | `minimal.py` 使用的默认模型;`--model` 优先 |
|
||||
| `DSH_SESSION_ROOT` | JSONL 会话目录 |
|
||||
| `DSH_SYSTEM_PROMPT` | 由部署提供的编码人格 |
|
||||
随附的 [`@deepseek-ai/dsh-sdk-minimal` 组合包](../../packages/bundle/sdk-minimal/README.zh.md)是该模式完整且显式的 Cordis 配置树。它只暴露:
|
||||
|
||||
通过 Python SDK 的 `cordis` 选项或 `DSH_CORDIS_CONFIG` 传入配置路径。内置可执行文件已携带此文件中指定的每个插件;目标机器无需 Node.js。
|
||||
- agent 所有的持久 `bash`
|
||||
- 支持 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`
|
||||
|
||||
## 极简变体
|
||||
该组合包不包含 `dsh-base`,因此每一个新增配置项都是显式 profile 变更。运行时上下文、本地指令发现、compaction、settings、托管凭据、遥测、Web 工具、subagent 与完整默认工具清单均不存在。配置树保留 SDK 启动与 JSON-RPC 服务、一个由环境配置的 DeepSeek 适配器、本地执行和 JSONL 持久化。
|
||||
|
||||
[`minimal.cordis.yml`](minimal.cordis.yml) 是 Web `minimal` preset 的完整独立版本。`DSH_SYSTEM_PROMPT` 选择它的系统提示词,未设置时使用 `You are a helpful software engineer assistant.`。它为新建会话抑制每个 system-prompt runtime-context 贡献,且不挂载上下文压缩插件。面向模型的工具严格只有:
|
||||
此变体刻意只支持 POSIX。其持久 PTY 与 editor 可以修改运行时进程可访问的任何路径,因此只应在一次性 checkout 或容器中使用。
|
||||
|
||||
- 所有者作用域内持久化的 `bash`
|
||||
- 提供 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor`
|
||||
## 添加插件
|
||||
|
||||
它组合了内置运行时所需的本地 PTY、裸 `fs-local` 后端、供持久 Bash 使用的 danger-full-access 策略,以及未压缩的 JSONL 持久化。Bash 和编辑器绝对路径可以修改运行时进程有权访问的任何路径,因此只能针对可丢弃的 checkout 或容器运行该变体。持久 PTY 需要 POSIX 终端环境,因此不适用于 Windows agent 接口。
|
||||
对同一个显式 home 使用运行时 wheel 提供的 `dsh` 命令,以进行持久 profile 变更:
|
||||
|
||||
[`minimal.py`](minimal.py)通过 Python SDK 运行该组合,并把 `DSH_MODEL` 作为默认模型。[Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)介绍安装、运行、workspace 选择与 session 标识;[SDK 参考](../../python/sdk/README.zh.md)归属运行时生命周期与结果语义。
|
||||
```sh
|
||||
export DSH_HOME=/absolute/path/to/example-dsh-home
|
||||
dsh plugin --profile sdk-minimal add file:/absolute/path/to/my-plugin-bundle
|
||||
```
|
||||
|
||||
在该命令中使用 `sdk-minimal` 可扩展本示例,使用 `sdk` 则扩展基于完整 base 的 SDK profile。Python 调用也可以在 `patches=(...)` 中传入更多绝对 patch 路径;后面的文件优先。所选 profile 必须保留 `@deepseek-ai/dsh-sdk-app` 或另一个 JSON-RPC server 配置项。该示例不接受完整 Cordis 文件或任意进程 argv。
|
||||
|
||||
同一个运行时 wheel 还为直接 CLI 使用打包 `web` profile 及其前端产物:`dsh web` 会启动这个独立应用。Python SDK client 不能选择 `web`,因为其中没有 JSON-RPC server 配置项。
|
||||
|
||||
另见 [Python SDK 教程](../../docs/user/guide/python-sdk.zh.md)与 [SDK 参考](../../python/sdk/README.zh.md)。
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
# key or network; every other entry remains shared. The replay provider
|
||||
# catalog claims the `deepseek-official` provider so the SDK server's `initialize`
|
||||
# finds it owned and never mounts the real-adapter fallback. The SDK snapshot
|
||||
# suite passes this path explicitly through `DSH_CORDIS_CONFIG` (the
|
||||
# jsonrpc-demo bin performs no DSH_SNAPSHOT config swap of its own), and
|
||||
# `llm-replay` reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the
|
||||
# harness. Stdout remains reserved for JSON-RPC frames.
|
||||
# suite selects this complete-config fixture explicitly, and `llm-replay`
|
||||
# reads `DSH_SNAPSHOT_FILE` / `DSH_SNAPSHOT_CHILD_FILES` from the harness.
|
||||
# It is not a Python launch interface. Stdout remains reserved for JSON-RPC.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Unattended coding-agent deployment for the bundled dsh-jsonrpc-agent runtime.
|
||||
# stdout is reserved for JSON-RPC; do not add a console logger or terminal UI.
|
||||
# Complete JSON-RPC composition fixture for lower-level Loader and SDK tests.
|
||||
# Python users launch `dsh --profile sdk` and apply patches instead.
|
||||
# Stdout is reserved for JSON-RPC; do not add a console logger or terminal UI.
|
||||
|
||||
- id: sdk-jsonrpc-server
|
||||
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
# Complete unattended minimal-agent composition for the Python SDK. The model
|
||||
# sees one deployment-selected system prompt and only the owner-scoped
|
||||
# persistent Bash and string-replace editor tools. Runtime-context injection and
|
||||
# context compaction are absent.
|
||||
|
||||
- id: sdk-jsonrpc-server
|
||||
name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
|
||||
config:
|
||||
maxTokensAsSuccess: false
|
||||
|
||||
- id: deepseek-llm-api-extensions
|
||||
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
|
||||
|
||||
- id: session-log-deepseek
|
||||
name: '@deepseek-ai/dsh-session-log-deepseek'
|
||||
|
||||
- id: plugin-package-inventory-deepseek
|
||||
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
|
||||
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
streamIdleTimeoutMs: 172800000
|
||||
models:
|
||||
- id: !!js process.env.DSH_MODEL ?? 'deepseek-v4-flash'
|
||||
contextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000)
|
||||
|
||||
- id: sandbox
|
||||
name: '@deepseek-ai/dsh-sandbox-local'
|
||||
|
||||
- id: sandbox-policy
|
||||
name: '@deepseek-ai/dsh-sandbox-policy'
|
||||
config:
|
||||
mode: danger-full-access
|
||||
workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd()
|
||||
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: pty
|
||||
name: '@deepseek-ai/dsh-terminal'
|
||||
|
||||
- id: terminal-bash
|
||||
name: '@deepseek-ai/dsh-terminal-bash'
|
||||
config:
|
||||
timeoutMs: 300000
|
||||
|
||||
# The editor uses the bare local filesystem; persistent Bash still consumes the
|
||||
# shared danger-full-access sandbox policy above.
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
cwd: !!js process.env.DSH_CWD ?? process.cwd()
|
||||
|
||||
- id: agent-spine
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
includeHarnessIdentity: false
|
||||
includeRuntimeContext: false
|
||||
persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash: false
|
||||
toolJobs: false
|
||||
|
||||
- id: persistent-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash-persistent'
|
||||
config:
|
||||
timeoutMs: 300000
|
||||
description: |-
|
||||
Run commands in a bash shell
|
||||
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
||||
* You don't have access to the internet via this tool.
|
||||
* You do have access to a mirror of common linux and python packages via apt and pip.
|
||||
* State is persistent across command calls and discussions with the user.
|
||||
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
|
||||
* Please avoid commands that may produce a very large amount of output.
|
||||
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
|
||||
|
||||
- id: str-replace-editor
|
||||
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
||||
config:
|
||||
maxOutputChars: 16000
|
||||
|
||||
- id: sessions
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
|
||||
compression: none
|
||||
@@ -10,30 +10,35 @@ from pathlib import Path
|
||||
from deepseek_harness import DeepSeekHarness
|
||||
|
||||
|
||||
CONFIG = Path(__file__).with_name("minimal.cordis.yml")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Parse one task and print the agent's final response."""
|
||||
parser = argparse.ArgumentParser()
|
||||
configured_home = os.environ.get("DSH_HOME", "")
|
||||
parser.add_argument("prompt", help="Task for the minimal agent")
|
||||
parser.add_argument("--workspace", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--session-root", type=Path, default=Path(".dsh-sessions"))
|
||||
parser.add_argument(
|
||||
"--dsh-home",
|
||||
type=Path,
|
||||
default=Path(configured_home) if configured_home.strip() else None,
|
||||
)
|
||||
parser.add_argument("--profile", default="sdk-minimal")
|
||||
parser.add_argument("--session-id")
|
||||
parser.add_argument("--provider", default="deepseek-official")
|
||||
parser.add_argument("--model", default=os.environ.get("DSH_MODEL", "deepseek-v4-flash"))
|
||||
parser.add_argument("--max-tokens", type=int)
|
||||
args = parser.parse_args()
|
||||
if args.dsh_home is None:
|
||||
parser.error("--dsh-home or a non-empty DSH_HOME is required")
|
||||
|
||||
workspace = args.workspace.resolve()
|
||||
session_root = args.session_root.resolve()
|
||||
dsh_home = args.dsh_home.resolve()
|
||||
with DeepSeekHarness(
|
||||
provider=args.provider,
|
||||
model=args.model,
|
||||
max_tokens=args.max_tokens,
|
||||
cwd=str(workspace),
|
||||
session_root=str(session_root),
|
||||
cordis=str(CONFIG.resolve()),
|
||||
dsh_home=str(dsh_home),
|
||||
profile=args.profile,
|
||||
) as harness:
|
||||
result = harness.run(args.prompt, session_id=args.session_id)
|
||||
print(result.final_response)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Keyless replay keeps the complete minimal composition intact and replaces
|
||||
# only its live DeepSeek adapter with the fixture-backed provider. The replay
|
||||
# catalog claims the same route initialized by the SDK.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
path: ./minimal.cordis.yml
|
||||
patches:
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
config:
|
||||
providers:
|
||||
- id: deepseek-official
|
||||
name: DeepSeek
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
@@ -8,8 +8,7 @@ import { zstdDecompress } from 'node:zlib'
|
||||
import { execa } from 'execa'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../packages/sdk/python-runtime/src/packaged-bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
||||
const repoRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
const decompress = promisify(zstdDecompress)
|
||||
|
||||
@@ -45,7 +44,7 @@ function waitForLine(
|
||||
})
|
||||
}
|
||||
|
||||
describe('Python SDK runtime carrier keyless smoke', () => {
|
||||
describe('Python SDK dsh profile keyless smoke', () => {
|
||||
it.each([
|
||||
{ label: 'reports max-token turns with the default mapping config', envValue: undefined },
|
||||
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true' },
|
||||
@@ -73,16 +72,18 @@ describe('Python SDK runtime carrier keyless smoke', () => {
|
||||
// execa owns spawn, the deadline, and exit settlement around it.
|
||||
const child = execa(process.execPath, [
|
||||
'--import',
|
||||
'tsx',
|
||||
'tsx/esm',
|
||||
binScript,
|
||||
configPath,
|
||||
'--profile',
|
||||
'sdk',
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
DSH_HOME: join(root, '.dsh'),
|
||||
DSH_PERMISSION_MODE: 'danger-full-access',
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
DSH_CWD: root,
|
||||
DSH_SESSION_ROOT: join(root, '.sessions'),
|
||||
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
|
||||
},
|
||||
timeout: 35_000,
|
||||
@@ -143,23 +144,14 @@ describe('Python SDK runtime carrier keyless smoke', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
const tools = modelRequests[0]?.tools as { function?: { name?: string } }[]
|
||||
expect(modelRequests[0]?.max_tokens).toBe(1234)
|
||||
expect(tools.map(tool => tool.function?.name).sort()).toEqual([
|
||||
'bash',
|
||||
'edit',
|
||||
'read',
|
||||
'subagent',
|
||||
'todo_write',
|
||||
'write',
|
||||
])
|
||||
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
|
||||
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
|
||||
expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} })
|
||||
const exit = await child
|
||||
expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0)
|
||||
const sessionsRoot = join(root, '.sessions')
|
||||
const sessionsRoot = join(root, '.dsh', 'sessions')
|
||||
const files = await readdir(sessionsRoot, { recursive: true })
|
||||
const log = files.find(file => file.endsWith('.jsonl.zstd'))
|
||||
expect(log).toBeDefined()
|
||||
@@ -175,28 +167,129 @@ describe('Python SDK runtime carrier keyless smoke', () => {
|
||||
}
|
||||
}, 40_000)
|
||||
|
||||
it('rejects an invalid max-token success env value', async () => {
|
||||
const { exitCode, stdout, stderr } = await execa(process.execPath, [
|
||||
it('boots the standalone minimal profile with its exact model-facing roster', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-minimal-'))
|
||||
const modelRequests: Record<string, unknown>[] = []
|
||||
const modelServer = createServer((request, response) => {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk: string) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
modelRequests.push(JSON.parse(body) as Record<string, unknown>)
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n')
|
||||
response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
|
||||
response.write('data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n')
|
||||
response.end('data: [DONE]\n\n')
|
||||
})
|
||||
})
|
||||
await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve))
|
||||
const address = modelServer.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('model server did not bind a TCP port')
|
||||
const child = execa(process.execPath, [
|
||||
'--import',
|
||||
'tsx',
|
||||
'tsx/esm',
|
||||
binScript,
|
||||
configPath,
|
||||
'--profile',
|
||||
'sdk-minimal',
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
DSH_HOME: join(root, '.dsh'),
|
||||
DSH_SYSTEM_PROMPT: 'Minimal allowlist prompt.',
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes',
|
||||
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
|
||||
},
|
||||
stdin: 'ignore',
|
||||
timeout: 25_000,
|
||||
timeout: 35_000,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
})
|
||||
const lines: string[] = []
|
||||
let stdoutBuffer = ''
|
||||
let stderr = ''
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
stdoutBuffer += chunk.toString('utf8')
|
||||
const parts = stdoutBuffer.split('\n')
|
||||
stdoutBuffer = parts.pop() ?? ''
|
||||
lines.push(...parts)
|
||||
})
|
||||
child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8') })
|
||||
|
||||
expect(exitCode, stderr).toBe(1)
|
||||
expect(stdout).toBe('')
|
||||
expect(stderr).toContain('plugin tree failed to load')
|
||||
expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)')
|
||||
expect(stderr).toContain('sometimes')
|
||||
try {
|
||||
child.stdin.write(`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: { cwd: root, provider: 'deepseek-official', model: 'deepseek-v4-pro' },
|
||||
})}\n`)
|
||||
await waitForLine(lines, value => value.id === 1, () => stderr)
|
||||
child.stdin.write(`${JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'session/prompt',
|
||||
params: { sessionId: 'minimal', contentBlocks: [{ type: 'text', text: 'inspect tools' }] },
|
||||
})}\n`)
|
||||
await waitForLine(lines, (value) => {
|
||||
const params = value.params as Record<string, unknown> | undefined
|
||||
const event = params?.event as Record<string, unknown> | undefined
|
||||
return params?.sessionId === 'minimal' && event?.type === 'turn/end'
|
||||
}, () => stderr)
|
||||
|
||||
const request = modelRequests[0] as {
|
||||
messages?: Array<{ role?: string; content?: unknown }>
|
||||
tools?: Array<{ function?: { name?: string } }>
|
||||
}
|
||||
expect(request.messages?.[0]).toMatchObject({ role: 'system', content: 'Minimal allowlist prompt.' })
|
||||
expect(request.tools?.map(tool => tool.function?.name).sort()).toEqual(['bash', 'str_replace_editor'])
|
||||
const profile = JSON.parse(
|
||||
await readFile(join(root, '.dsh', 'profiles', 'sdk-minimal', 'package.json'), 'utf8'),
|
||||
) as { dsh?: { profile?: { bundles?: string[]; patchReload?: string } } }
|
||||
expect(profile.dsh?.profile).toEqual({
|
||||
bundles: ['@deepseek-ai/dsh-sdk-minimal'],
|
||||
patchReload: 'startup',
|
||||
})
|
||||
|
||||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
|
||||
await waitForLine(lines, value => value.id === 3, () => stderr)
|
||||
const exit = await child
|
||||
expect(exit.exitCode, `signal=${String(exit.signal)}; stderr=${stderr}`).toBe(0)
|
||||
} finally {
|
||||
child.kill('SIGKILL')
|
||||
await child
|
||||
await new Promise<void>(resolve => modelServer.close(() => { resolve() }))
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 40_000)
|
||||
|
||||
it('rejects an invalid max-token success env value', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-invalid-'))
|
||||
try {
|
||||
const { exitCode, stdout, stderr } = await execa(process.execPath, [
|
||||
'--import',
|
||||
'tsx/esm',
|
||||
binScript,
|
||||
'--profile',
|
||||
'sdk',
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
DSH_HOME: join(root, '.dsh'),
|
||||
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
|
||||
DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes',
|
||||
},
|
||||
stdin: 'ignore',
|
||||
timeout: 25_000,
|
||||
killSignal: 'SIGKILL',
|
||||
reject: false,
|
||||
})
|
||||
|
||||
expect(exitCode, stderr).toBe(1)
|
||||
expect(stdout).toBe('')
|
||||
expect(stderr).toContain('plugin tree failed to load')
|
||||
expect(stderr).toContain('failed to apply loader entry sdk-jsonrpc-server (@deepseek-ai/dsh-sdk-jsonrpc-server)')
|
||||
expect(stderr).toContain('sometimes')
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}, 30_000)
|
||||
})
|
||||
|
||||
@@ -550,11 +550,6 @@
|
||||
"zod"
|
||||
]
|
||||
},
|
||||
"packages/sdk/python-runtime": {
|
||||
"project": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
},
|
||||
"packages/subagent/subagent-spawn-in-process": {
|
||||
"entry": [
|
||||
"tests/**/*.spec.ts",
|
||||
@@ -704,6 +699,11 @@
|
||||
"@deepseek-ai/dsh-sdk-jsonrpc-server"
|
||||
]
|
||||
},
|
||||
"packages/bundle/sdk-minimal": {
|
||||
"ignoreDependencies": [
|
||||
"@deepseek-ai/.+"
|
||||
]
|
||||
},
|
||||
"packages/bundle/web-app": {
|
||||
"ignoreDependencies": [
|
||||
"@deepseek-ai/.+"
|
||||
|
||||
@@ -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 packages/README.md
|
||||
README.md: 5e44b3821d1272923f1545e697d91a434374d24a
|
||||
README.zh.md: 1bf06b993c0379ecebf8246a50e1fc5fe5f4fce1
|
||||
README.md: 6d1add5baa7c73033dadc4247a03b546e6ff9dfd
|
||||
README.zh.md: da2606b93cae1a6a642a6ee15ac5f0cfc8ce6457
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
|
||||
| [`credentials/`](credentials/README.md) | Credential reference/record seam + env-over-`.env` provider + authorization flows | Product — stable API |
|
||||
| [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable API |
|
||||
| [`workspace/`](workspace/README.md) | Workspace entity | Product — stable API |
|
||||
| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol, TypeScript client/server, and private Python carrier | Product — stable API |
|
||||
| [`sdk/`](sdk/README.md) | Out-of-process SDK: JSON-RPC protocol and TypeScript client/server | Product — stable API |
|
||||
| [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable API |
|
||||
| [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable API |
|
||||
| [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable API |
|
||||
|
||||
@@ -50,7 +50,7 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通
|
||||
| [`credentials/`](credentials/README.zh.md) | 凭据引用/记录 seam + 环境变量优先于 `.env` 的提供方 + 授权 flow | 产品:稳定 API |
|
||||
| [`storage/`](storage/README.zh.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定 API |
|
||||
| [`workspace/`](workspace/README.zh.md) | Workspace 实体 | 产品:稳定 API |
|
||||
| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议、TypeScript 客户端/服务器和私有 Python 载体 | 产品:稳定 API |
|
||||
| [`sdk/`](sdk/README.zh.md) | 进程外 SDK:JSON-RPC 协议与 TypeScript 客户端/服务器 | 产品:稳定 API |
|
||||
| [`acp/`](acp/README.zh.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 | 产品:稳定 API |
|
||||
| [`interaction/`](interaction/README.zh.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 | 产品:稳定 API |
|
||||
| [`boot/`](boot/README.zh.md) | 共享的 app bin 启动粘合层 | 产品:稳定 API |
|
||||
|
||||
@@ -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 packages/boot/app-boot/README.md
|
||||
README.md: 9965d6d57f4ec6cd9a93650bbd0096b059fb010b
|
||||
README.zh.md: 436b19e4b2a05f4462f7636fccd3911b8ed41548
|
||||
README.md: a7c6272fd96fdf24b4f21bb8b60c087af4a4dc96
|
||||
README.zh.md: 09a63765414ba76d78a7c88f3777f63eecd835f7
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles and the [temporarily packaged Python SDK runtime](../../../python/README.md). The product launcher owns profile composition and process lifecycle; the direct-config helpers remain only for that held-back runtime until its later migration.
|
||||
Shared Loader boot glue for [`dsh`](../../../apps/cli/README.md) profiles, including the CLI packaged by the [Python runtime wheel](../../../python/README.md). The product launcher owns profile composition and process lifecycle. Direct-config helpers serve lower-level embedders and tests; they do not define another supported application entrypoint.
|
||||
|
||||
| Export | Role |
|
||||
|---|---|
|
||||
@@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](..
|
||||
|
||||
## Profiles
|
||||
|
||||
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
|
||||
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`sdk-minimal`/`acp` with startup-only patches; `sdk-minimal` lists only its standalone bundle, while the other templates retain their base-plus-mode stacks. Other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
|
||||
|
||||
User-level machine-local preferences also live in the Harness home:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
供 [`dsh`](../../../apps/cli/README.zh.md) profile 与[暂时打包的 Python SDK runtime](../../../python/README.zh.md) 共用的 Loader 启动粘合层。产品启动器负责 profile 组合与进程生命周期;直接配置 helper 只为暂缓迁移的 runtime 保留,直至后续迁移。
|
||||
供 [`dsh`](../../../apps/cli/README.zh.md) profile 共用的 Loader 启动粘合层,也用于 [Python 运行时 wheel](../../../python/README.zh.md)打包的 CLI。产品启动器负责 profile 组合与进程生命周期。直接配置 helper 服务于底层 embedder 与测试,不会定义另一个受支持的应用入口。
|
||||
|
||||
| 导出 | 职责 |
|
||||
|---|---|
|
||||
@@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面
|
||||
|
||||
## Profiles
|
||||
|
||||
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
|
||||
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`sdk-minimal`/`acp`;`sdk-minimal` 只列出自己的独立组合包,其他模板保留 base 加模式层的组合。其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
|
||||
|
||||
用户级的机器本地偏好同样位于 harness home 中:
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.2.0"
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:^",
|
||||
"js-yaml": "^4.2.0",
|
||||
"resolve.exports": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-group": "workspace:^",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Shared boot glue for `dsh` profiles and the temporarily packaged Python SDK runtime: load the gitignored
|
||||
* Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored
|
||||
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
|
||||
* optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to
|
||||
* config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles.
|
||||
@@ -811,7 +811,9 @@ export async function boot(
|
||||
// original activation error instead of only the wrap chain.
|
||||
let deepest: unknown = cause
|
||||
while (deepest instanceof Error && deepest.cause !== undefined) deepest = deepest.cause
|
||||
const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : ''
|
||||
const stack = deepest instanceof AggregateError
|
||||
? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join('\n')}`
|
||||
: deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : ''
|
||||
throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,15 @@
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import {
|
||||
existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync,
|
||||
existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports'
|
||||
import { loadOverlayPatches } from './index.ts'
|
||||
|
||||
/** Directory under the Harness home holding every profile. */
|
||||
@@ -143,6 +146,10 @@ export const PROFILE_TEMPLATES: Record<string, ProfileTemplate> = {
|
||||
bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'],
|
||||
patchReload: 'startup',
|
||||
},
|
||||
'sdk-minimal': {
|
||||
bundles: ['@deepseek-ai/dsh-sdk-minimal'],
|
||||
patchReload: 'startup',
|
||||
},
|
||||
}
|
||||
|
||||
/** Installation-owned bundle tuples normalized to the shipped template. */
|
||||
@@ -204,7 +211,16 @@ export function initProfile(
|
||||
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE)
|
||||
}
|
||||
|
||||
/** Ensure `link` is a symlink to `target`, replacing a wrong or dangling link; a real directory throws. */
|
||||
function readModuleProxyRecord(link: string): ModuleProxyRecord | undefined {
|
||||
try {
|
||||
return JSON.parse(readFileSync(join(link, 'package.json'), 'utf8')) as ModuleProxyRecord
|
||||
} catch {
|
||||
// Missing or invalid metadata is not managed state; callers reject it.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */
|
||||
function ensureSymlink(link: string, target: string): void {
|
||||
let stat
|
||||
try {
|
||||
@@ -216,12 +232,19 @@ function ensureSymlink(link: string, target: string): void {
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
if (!stat.isSymbolicLink()) {
|
||||
throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`)
|
||||
const existing = stat.isDirectory() ? readModuleProxyRecord(link) : undefined
|
||||
if (existing?.dsh?.moduleFallback?.targets === undefined) {
|
||||
throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
|
||||
}
|
||||
rmSync(link, { recursive: true })
|
||||
stat = undefined
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
if (readlinkSync(link) === target) return
|
||||
// unlink deletes the reparse point itself on Windows too; rmSync treats a
|
||||
// junction as a directory and throws EISDIR unless recursive.
|
||||
unlinkSync(link)
|
||||
}
|
||||
if (readlinkSync(link) === target) return
|
||||
// unlink deletes the reparse point itself on Windows too; rmSync treats a
|
||||
// junction as a directory and throws EISDIR unless recursive.
|
||||
unlinkSync(link)
|
||||
}
|
||||
try {
|
||||
symlinkSync(target, link, 'junction')
|
||||
@@ -238,29 +261,162 @@ function ensureSymlink(link: string, target: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
interface ModuleProxyManifest {
|
||||
name: string
|
||||
version: string
|
||||
private: true
|
||||
type: 'module'
|
||||
exports: Record<string, string>
|
||||
dsh: { moduleFallback: { targets: Record<string, string> } }
|
||||
}
|
||||
|
||||
interface ModuleProxyRecord {
|
||||
version?: unknown
|
||||
dsh?: { moduleFallback?: { targets?: unknown } }
|
||||
}
|
||||
|
||||
/** Return whether the process reads application modules from pkg's virtual filesystem. */
|
||||
function isPackagedExecutable(): boolean {
|
||||
return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
|
||||
}
|
||||
|
||||
/** Resolve one available explicit package export under Node ESM import conditions. */
|
||||
function packageEntryFromPackage(
|
||||
packageName: string,
|
||||
packageDir: string,
|
||||
declared: ResolvePackageManifest['exports'],
|
||||
subpath: string,
|
||||
): string | undefined {
|
||||
let candidates: string[] | void
|
||||
try {
|
||||
candidates = resolvePackage({ name: packageName, exports: declared }, subpath)
|
||||
} catch (error) {
|
||||
if ((error as Error).message.startsWith('No known conditions for ')) return undefined
|
||||
const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1)
|
||||
throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error })
|
||||
}
|
||||
for (const candidate of candidates ?? []) {
|
||||
const target = candidate
|
||||
const entry = resolve(packageDir, target)
|
||||
const relativeEntry = relative(packageDir, entry)
|
||||
if (!target.startsWith('./') || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) {
|
||||
throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`)
|
||||
}
|
||||
if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */
|
||||
function packageProxySource(
|
||||
packageName: string,
|
||||
packageDir: string,
|
||||
): { version: string; targets: Record<string, string> } {
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
|
||||
bin?: unknown
|
||||
exports?: unknown
|
||||
main?: unknown
|
||||
types?: unknown
|
||||
typings?: unknown
|
||||
version?: unknown
|
||||
}
|
||||
if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
|
||||
throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`)
|
||||
}
|
||||
const declared = manifest.exports
|
||||
if (declared === undefined) {
|
||||
const main = typeof manifest.main === 'string' && manifest.main.length > 0 ? manifest.main : undefined
|
||||
const entry = join(packageDir, main ?? 'index')
|
||||
try {
|
||||
const resolved = createRequire(join(packageDir, 'package.json')).resolve(entry)
|
||||
return { version: manifest.version, targets: { '.': pathToFileURL(resolved).href } }
|
||||
} catch (error) {
|
||||
if (main === undefined
|
||||
&& (manifest.bin !== undefined || manifest.types !== undefined || manifest.typings !== undefined)) {
|
||||
return { version: manifest.version, targets: {} }
|
||||
}
|
||||
throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error })
|
||||
}
|
||||
}
|
||||
const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared)
|
||||
&& Object.keys(declared).some(key => key.startsWith('.'))
|
||||
? Object.keys(declared).filter(key => key === '.' || (
|
||||
key.startsWith('./') && !key.includes('*') && !key.endsWith('/') && key !== './package.json'
|
||||
))
|
||||
: ['.']
|
||||
const targets: Record<string, string> = {}
|
||||
for (const subpath of subpaths) {
|
||||
const target = packageEntryFromPackage(
|
||||
packageName,
|
||||
packageDir,
|
||||
declared as ResolvePackageManifest['exports'],
|
||||
subpath,
|
||||
)
|
||||
if (target !== undefined) targets[subpath] = target
|
||||
}
|
||||
return { version: manifest.version, targets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
|
||||
* symlink per package in the dsh app's resolvable dependency CLOSURE (BFS
|
||||
* over `dependencies` from the app manifest), each resolved from its own
|
||||
* real location. Node's parent-directory walk from any profile finds this
|
||||
* directory after the profile's own `node_modules`, so every in-box plugin
|
||||
* resolves without pnpm ever managing it — the exact "bundles come from the
|
||||
* installation" contract. The closure (not just direct dependencies) is
|
||||
* required for out-of-tree plugins: their peer dependencies name Service
|
||||
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
|
||||
* reaches only through its Service Provider packages. Symlinked packages
|
||||
* resolve their own dependencies from their real directories (Node's default
|
||||
* symlink-following), so each package needs only its one flat link.
|
||||
* Idempotent: correct links are kept and moved installations are
|
||||
* re-pointed; a stale link to a vanished package stays until its name is
|
||||
* reused (dangling links are invisible to resolution).
|
||||
* @param installAnchor - absolute path of the dsh app's package.json.
|
||||
* @param home - the Harness home; defaults to {@link resolveDshHome}.
|
||||
* Materialize a real package proxy whose exports retain pkg's virtual module
|
||||
* URL. Files outside the executable cannot traverse a symlink into
|
||||
* `/snapshot`, while an ESM re-export can import that URL and preserves the
|
||||
* executable's single module instance for out-of-tree plugin peers.
|
||||
*/
|
||||
export function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): void {
|
||||
const profilesDir = join(home, PROFILES_DIR)
|
||||
const modulesDir = join(profilesDir, 'node_modules')
|
||||
mkdirSync(modulesDir, { recursive: true })
|
||||
function ensureModuleProxy(
|
||||
link: string,
|
||||
packageName: string,
|
||||
version: string,
|
||||
targets: Record<string, string>,
|
||||
): void {
|
||||
const proxyExports = Object.fromEntries(
|
||||
Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`]),
|
||||
)
|
||||
const manifest: ModuleProxyManifest = {
|
||||
name: packageName,
|
||||
version,
|
||||
private: true,
|
||||
type: 'module',
|
||||
exports: proxyExports,
|
||||
dsh: { moduleFallback: { targets } },
|
||||
}
|
||||
let stat
|
||||
try {
|
||||
stat = lstatSync(link)
|
||||
} catch {
|
||||
stat = undefined
|
||||
}
|
||||
if (stat?.isSymbolicLink()) {
|
||||
unlinkSync(link)
|
||||
stat = undefined
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
const existing = readModuleProxyRecord(link)
|
||||
if (existing?.dsh?.moduleFallback?.targets === undefined) {
|
||||
throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
|
||||
}
|
||||
if (existing.version === version
|
||||
&& JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)
|
||||
&& Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return
|
||||
rmSync(link, { recursive: true })
|
||||
}
|
||||
mkdirSync(link, { recursive: true })
|
||||
writeFileSync(join(link, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n')
|
||||
for (const [index, target] of Object.values(targets).entries()) {
|
||||
const specifier = JSON.stringify(target)
|
||||
writeFileSync(
|
||||
join(link, `entry-${index}.js`),
|
||||
`export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleFallbackEntry =
|
||||
| { kind: 'symlink'; packageName: string; packageDir: string }
|
||||
| { kind: 'proxy'; packageName: string; version: string; targets: Record<string, string> }
|
||||
|
||||
/** Resolve the installation generation that every profile must find through the fallback directory. */
|
||||
function resolveModuleFallbackEntries(installAnchor: string): ModuleFallbackEntry[] {
|
||||
const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest
|
||||
const links = new Map<string, string>()
|
||||
/* v8 ignore next -- a real app manifest always declares its name */
|
||||
@@ -284,10 +440,89 @@ export function healProfilesModuleFallback(installAnchor: string, home: string =
|
||||
queue.push({ anchor: manifestPath, manifest: JSON.parse(readFileSync(manifestPath, 'utf8')) as ProfileManifest })
|
||||
}
|
||||
}
|
||||
for (const [packageName, target] of links) {
|
||||
const link = join(modulesDir, packageName)
|
||||
if (!isPackagedExecutable()) {
|
||||
return [...links].map(([packageName, packageDir]) => ({ kind: 'symlink', packageName, packageDir }))
|
||||
}
|
||||
return [...links].flatMap(([packageName, packageDir]) => {
|
||||
const source = packageProxySource(packageName, packageDir)
|
||||
return Object.keys(source.targets).length === 0
|
||||
? []
|
||||
: [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }]
|
||||
})
|
||||
}
|
||||
|
||||
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
||||
function moduleFallbackEntryCurrent(modulesDir: string, entry: ModuleFallbackEntry): boolean {
|
||||
const link = join(modulesDir, entry.packageName)
|
||||
try {
|
||||
const stat = lstatSync(link)
|
||||
if (entry.kind === 'symlink') {
|
||||
return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir
|
||||
}
|
||||
if (!stat.isDirectory()) return false
|
||||
const existing = readModuleProxyRecord(link)
|
||||
return existing?.version === entry.version
|
||||
&& JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets)
|
||||
&& Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether every required fallback entry is already ready for this installation. */
|
||||
function moduleFallbackCurrent(modulesDir: string, entries: readonly ModuleFallbackEntry[]): boolean {
|
||||
return entries.every(entry => moduleFallbackEntryCurrent(modulesDir, entry))
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
|
||||
* entry per package in the dsh app's resolvable dependency CLOSURE (BFS
|
||||
* over `dependencies` from the app manifest), each resolved from its own
|
||||
* installation location. Plain Node uses symlinks. A pkg executable resolves
|
||||
* exports under ESM import conditions and writes small proxy packages because
|
||||
* the host filesystem cannot follow a symlink into pkg's virtual `/snapshot`
|
||||
* tree; the proxy re-exports the virtual URL, preserving the executable's
|
||||
* single module instance. A complete matching generation returns without a
|
||||
* writer lock; actual repairs acquire and recheck one cross-process lock so
|
||||
* partial proxies and carrier transitions remain serialized. Node's
|
||||
* parent-directory walk from any profile finds this
|
||||
* directory after the profile's own `node_modules`, so every in-box plugin
|
||||
* resolves without pnpm ever managing it — the exact "bundles come from the
|
||||
* installation" contract. The closure (not just direct dependencies) is
|
||||
* required for out-of-tree plugins: their peer dependencies name Service
|
||||
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
|
||||
* reaches only through its Service Provider packages. Both a symlink target
|
||||
* and a proxy's virtual target resolve transitive imports from the original
|
||||
* package directory, so each package needs one flat fallback entry.
|
||||
* Idempotent: correct entries are kept and changed installation targets are
|
||||
* rewritten; under plain Node, a stale dangling link stays until its name is
|
||||
* reused because resolution cannot discover it.
|
||||
* @param installAnchor - absolute path of the dsh app's package.json.
|
||||
* @param home - the Harness home; defaults to {@link resolveDshHome}.
|
||||
* @returns settlement after current-state validation or a locked repair.
|
||||
*/
|
||||
export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise<void> {
|
||||
const profilesDir = join(home, PROFILES_DIR)
|
||||
const modulesDir = join(profilesDir, 'node_modules')
|
||||
mkdirSync(modulesDir, { recursive: true })
|
||||
const entries = resolveModuleFallbackEntries(installAnchor)
|
||||
if (moduleFallbackCurrent(modulesDir, entries)) return
|
||||
await withFileLock(modulesDir, () => {
|
||||
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir)
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/** Heal one module-fallback generation while the cross-process writer lock is held. */
|
||||
function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[], modulesDir: string): void {
|
||||
for (const entry of entries) {
|
||||
const link = join(modulesDir, entry.packageName)
|
||||
mkdirSync(dirname(link), { recursive: true })
|
||||
ensureSymlink(link, target)
|
||||
if (entry.kind === 'proxy') {
|
||||
ensureModuleProxy(link, entry.packageName, entry.version, entry.targets)
|
||||
} else {
|
||||
ensureSymlink(link, entry.packageDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -771,6 +771,28 @@ describe('boot', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('expands a stackless aggregate at the deepest activation cause', async () => {
|
||||
const dir = tmp()
|
||||
const aggregate = new AggregateError([
|
||||
new Error('first aggregate member'),
|
||||
'second aggregate member',
|
||||
], 'aggregate activation failure')
|
||||
delete (aggregate as { stack?: string }).stack
|
||||
try {
|
||||
await boot(NAME, join(dir, 'cordis.yml'), undefined, () => {
|
||||
throw new Error('wrapped aggregate failure', { cause: aggregate })
|
||||
})
|
||||
expect.fail('boot should reject the aggregate activation failure')
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
const message = (error as Error).message
|
||||
expect(message).toContain(`${NAME}: host preparation failed: wrapped aggregate failure`)
|
||||
expect(message).toContain('aggregate activation failure')
|
||||
expect(message).toContain('first aggregate member')
|
||||
expect(message).toContain('second aggregate member')
|
||||
}
|
||||
})
|
||||
|
||||
it('reports a pending real Loader fiber and the service unresolved in its own context', async () => {
|
||||
const dir = tmp()
|
||||
writeFileSync(join(dir, 'waiting.mjs'), 'export const inject = ["neverProvided"]\nexport function apply() {}\n')
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* empty-root composition, and the installation module-fallback healing.
|
||||
*/
|
||||
|
||||
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
composeEntries,
|
||||
@@ -36,12 +37,18 @@ function stageInstallation(bundles: Record<string, { patch?: string; deps?: Reco
|
||||
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
||||
name,
|
||||
version: '0.0.0',
|
||||
type: 'module',
|
||||
main: './index.js',
|
||||
dependencies: spec.deps ?? {},
|
||||
...spec.patch === undefined ? {} : { dsh: { bundle: { patch: './cordis.patch.yml' } } },
|
||||
}))
|
||||
writeFileSync(join(dir, 'index.js'), `export const packageName = ${JSON.stringify(name)}\n`)
|
||||
if (spec.patch !== undefined) writeFileSync(join(dir, 'cordis.patch.yml'), spec.patch)
|
||||
}
|
||||
writeFileSync(join(appDir, 'package.json'), JSON.stringify({ name: 'dsh-app', dependencies: appDeps }))
|
||||
writeFileSync(join(appDir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-app', version: '0.0.0', type: 'module', main: './index.js', dependencies: appDeps,
|
||||
}))
|
||||
writeFileSync(join(appDir, 'index.js'), 'export const packageName = "dsh-app"\n')
|
||||
return join(appDir, 'package.json')
|
||||
}
|
||||
|
||||
@@ -166,6 +173,10 @@ describe('loadProfile', () => {
|
||||
bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-sdk-app'],
|
||||
patchReload: 'startup',
|
||||
})
|
||||
expect(PROFILE_TEMPLATES['sdk-minimal']).toEqual({
|
||||
bundles: ['@deepseek-ai/dsh-sdk-minimal'],
|
||||
patchReload: 'startup',
|
||||
})
|
||||
try {
|
||||
loadProfile('t', 'web', anchor, home)
|
||||
} catch {
|
||||
@@ -265,7 +276,7 @@ describe('composeEntries', () => {
|
||||
})
|
||||
|
||||
describe('healProfilesModuleFallback', () => {
|
||||
it('links the app and bundle dependency surface flat under profiles/node_modules', () => {
|
||||
it('links the app and bundle dependency surface flat under profiles/node_modules', async () => {
|
||||
const anchor = stageInstallation({
|
||||
'bundle-a': { patch: '[]\n', deps: { 'dep-of-a': '0.0.0', 'ghost-dep': '0.0.0' } },
|
||||
'plain-lib': {},
|
||||
@@ -279,7 +290,7 @@ describe('healProfilesModuleFallback', () => {
|
||||
mkdirSync(join(modules, 'dep-of-a'), { recursive: true })
|
||||
writeFileSync(join(modules, 'dep-of-a', 'package.json'), JSON.stringify({ name: 'dep-of-a', version: '0.0.0' }))
|
||||
const home = tmp()
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
// App deps, the bundle's own deps, and the bundle itself are linked; the
|
||||
// plain library is linked as an app dep (harmless), the app itself too.
|
||||
@@ -287,39 +298,403 @@ describe('healProfilesModuleFallback', () => {
|
||||
expect(lstatSync(join(fallback, name)).isSymbolicLink(), name).toBe(true)
|
||||
}
|
||||
// Idempotent, and a moved target is re-pointed.
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const before = readlinkSync(join(fallback, 'dep-of-a'))
|
||||
expect(before).toContain('dep-of-a')
|
||||
})
|
||||
|
||||
it('throws when a fallback entry is a real directory', () => {
|
||||
it('throws when a fallback entry is a foreign file or directory', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
mkdirSync(join(home, 'profiles', 'node_modules', 'dsh-app'), { recursive: true })
|
||||
expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow('is not a symlink')
|
||||
for (const kind of ['file', 'directory']) {
|
||||
const home = tmp()
|
||||
const entry = join(home, 'profiles', 'node_modules', 'dsh-app')
|
||||
mkdirSync(join(entry, '..'), { recursive: true })
|
||||
if (kind === 'directory') mkdirSync(entry)
|
||||
else writeFileSync(entry, '')
|
||||
await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow('is not a symlink')
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces a wrong symlink', () => {
|
||||
it('replaces a wrong symlink', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
mkdirSync(fallback, { recursive: true })
|
||||
symlinkSync(tmp(), join(fallback, 'dsh-app'), 'junction')
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app')
|
||||
})
|
||||
|
||||
it('tolerates losing the concurrent-heal race to an identical link and rejects a different one', () => {
|
||||
// The EEXIST arm: a second process wrote the link between our lstat miss
|
||||
// and symlinkSync. Simulated by pre-creating the correct link and calling
|
||||
// the internal path through a stale-lstat shim is not possible from
|
||||
// outside, so probe the observable contract: healing twice concurrently
|
||||
// is a no-op, and a foreign REAL directory still fails loud.
|
||||
it('retains current links while repairing a missing sibling', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const appTarget = readlinkSync(join(fallback, 'dsh-app'))
|
||||
unlinkSync(join(fallback, 'bundle-a'))
|
||||
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
|
||||
expect(readlinkSync(join(fallback, 'dsh-app'))).toBe(appTarget)
|
||||
expect(lstatSync(join(fallback, 'bundle-a')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('serializes concurrent healers and retains the identical link', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
healProfilesModuleFallback(anchor, home)
|
||||
healProfilesModuleFallback(anchor, home) // second healer sees the correct link
|
||||
await Promise.all([
|
||||
healProfilesModuleFallback(anchor, home),
|
||||
healProfilesModuleFallback(anchor, home),
|
||||
])
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not acquire the writer lock for a complete generation', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const modules = join(home, 'profiles', 'node_modules')
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
let releaseLock: (() => void) | undefined
|
||||
let reportLock: (() => void) | undefined
|
||||
const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
|
||||
const release = new Promise<void>((resolve) => { releaseLock = resolve })
|
||||
const holder = withFileLock(modules, async () => {
|
||||
reportLock?.()
|
||||
await release
|
||||
})
|
||||
await lockHeld
|
||||
|
||||
const healer = healProfilesModuleFallback(anchor, home)
|
||||
const outcome = await Promise.race([
|
||||
healer.then(() => 'complete' as const),
|
||||
new Promise<'blocked'>(resolve => setTimeout(() => { resolve('blocked') }, 100)),
|
||||
])
|
||||
releaseLock?.()
|
||||
await Promise.all([holder, healer])
|
||||
expect(outcome).toBe('complete')
|
||||
})
|
||||
|
||||
it('waits for the module-fallback writer lock before publishing entries', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const modules = join(home, 'profiles', 'node_modules')
|
||||
mkdirSync(modules, { recursive: true })
|
||||
let releaseLock: (() => void) | undefined
|
||||
let reportLock: (() => void) | undefined
|
||||
const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
|
||||
const release = new Promise<void>((resolve) => { releaseLock = resolve })
|
||||
const holder = withFileLock(modules, async () => {
|
||||
reportLock?.()
|
||||
await release
|
||||
})
|
||||
await lockHeld
|
||||
|
||||
const healer = healProfilesModuleFallback(anchor, home)
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(existsSync(join(modules, 'dsh-app'))).toBe(false)
|
||||
releaseLock?.()
|
||||
await Promise.all([holder, healer])
|
||||
expect(lstatSync(join(modules, 'dsh-app')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('writes real ESM proxies for a packaged executable', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
bundleManifest.exports = {
|
||||
'.': './index.js',
|
||||
'./feature': './feature.js',
|
||||
'./legacy/': './legacy/',
|
||||
'./types': { types: './feature.d.ts' },
|
||||
}
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest))
|
||||
writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n')
|
||||
const home = tmp()
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
const proxy = join(fallback, 'bundle-a')
|
||||
expect(lstatSync(proxy).isDirectory()).toBe(true)
|
||||
const proxyManifest = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
|
||||
version: unknown
|
||||
exports: unknown
|
||||
dsh: { moduleFallback: { targets: Record<string, unknown> } }
|
||||
}
|
||||
expect(proxyManifest).toMatchObject({
|
||||
version: '0.0.0',
|
||||
exports: { '.': './entry-0.js', './feature': './entry-1.js' },
|
||||
})
|
||||
expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js'))
|
||||
await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' })
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves import-only exports from each package installation', async () => {
|
||||
const anchor = stageInstallation({
|
||||
'bundle-a': { patch: '[]\n', deps: { 'nested-esm': '0.0.0' } },
|
||||
})
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
bundleManifest.exports = { '.': { import: './index.js' } }
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest))
|
||||
const nestedDir = join(bundleDir, 'node_modules', 'nested-esm')
|
||||
mkdirSync(nestedDir, { recursive: true })
|
||||
writeFileSync(join(nestedDir, 'package.json'), JSON.stringify({
|
||||
name: 'nested-esm',
|
||||
version: '0.0.0',
|
||||
type: 'module',
|
||||
exports: { import: './index.js' },
|
||||
}))
|
||||
writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "proxied"\n')
|
||||
const home = tmp()
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
await expect(import(join(fallback, 'bundle-a', 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(import(join(fallback, 'nested-esm', 'entry-0.js'))).resolves.toMatchObject({ nested: 'proxied' })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves explicit condition targets without filesystem package lookup', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
manifest.exports = {
|
||||
'.': { import: './index.js', require: './index.cjs' },
|
||||
'./mini': { types: './mini/index.d.ts', import: './mini/index.js', require: './mini/index.cjs' },
|
||||
'./web': { types: './dist/web/web.d.ts', import: './dist/web/index.mjs', default: './dist/web/index.mjs' },
|
||||
}
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
mkdirSync(join(bundleDir, 'mini'))
|
||||
writeFileSync(join(bundleDir, 'mini', 'index.js'), 'export const mini = true\n')
|
||||
mkdirSync(join(bundleDir, 'dist', 'web'), { recursive: true })
|
||||
writeFileSync(join(bundleDir, 'dist', 'web', 'index.mjs'), 'export const web = true\n')
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ mini: true })
|
||||
await expect(import(join(proxy, 'entry-2.js'))).resolves.toMatchObject({ web: true })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves the installation path while resolving packaged exports', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const appDir = join(anchor, '..')
|
||||
const physical = tmp()
|
||||
writeFileSync(join(physical, 'package.json'), JSON.stringify({
|
||||
name: 'linked-esm',
|
||||
version: '0.0.0',
|
||||
type: 'module',
|
||||
exports: { import: './index.js' },
|
||||
}))
|
||||
writeFileSync(join(physical, 'index.js'), 'export const linked = true\n')
|
||||
symlinkSync(physical, join(appDir, 'node_modules', 'linked-esm'), 'junction')
|
||||
const appManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies: Record<string, string> }
|
||||
appManifest.dependencies['linked-esm'] = '0.0.0'
|
||||
writeFileSync(anchor, JSON.stringify(appManifest))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const proxyManifest = JSON.parse(readFileSync(
|
||||
join(home, 'profiles', 'node_modules', 'linked-esm', 'package.json'),
|
||||
'utf8',
|
||||
)) as { dsh: { moduleFallback: { targets: Record<string, string> } } }
|
||||
expect(proxyManifest.dsh.moduleFallback.targets['.']).toContain('/app/node_modules/linked-esm/index.js')
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('uses the legacy index fallback when a package has no exports or main', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
delete manifest.main
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js')))
|
||||
.resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('uses Node legacy resolution for an extensionless main entry', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
manifest.main = './index'
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js')))
|
||||
.resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('skips executable-only and declaration-only packages without import entries', async () => {
|
||||
for (const marker of ['bin', 'types', 'typings']) {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const manifest = JSON.parse(readFileSync(anchor, 'utf8')) as Record<string, unknown>
|
||||
delete manifest.main
|
||||
manifest[marker] = marker === 'bin' ? { dsh: './lib/bin.js' } : './index.d.ts'
|
||||
if (marker === 'types') manifest.main = ''
|
||||
writeFileSync(anchor, JSON.stringify(manifest))
|
||||
rmSync(join(anchor, '..', 'index.js'))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
expect(existsSync(join(fallback, 'dsh-app'))).toBe(false)
|
||||
expect(existsSync(join(fallback, 'bundle-a', 'entry-0.js'))).toBe(true)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('fails loud on a missing legacy main entry', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
delete manifest.main
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
rmSync(join(bundleDir, 'index.js'))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow('main entry is missing')
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('omits unavailable ESM exports and rejects malformed export targets', async () => {
|
||||
for (const mode of ['missing', 'directory', 'absent-map', 'invalid', 'escape', 'null', 'null-subpath']) {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
const target = mode === 'missing' ? './missing.js'
|
||||
: mode === 'directory' ? './mini'
|
||||
: mode === 'escape' ? './../outside.js'
|
||||
: '../outside.js'
|
||||
manifest.exports = mode === 'absent-map' ? null
|
||||
: mode === 'null-subpath' ? { './bad': null }
|
||||
: { '.': mode === 'null' ? null : { import: target } }
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
if (mode === 'directory') mkdirSync(join(bundleDir, 'mini'))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
if (mode === 'missing' || mode === 'directory' || mode === 'absent-map') {
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules', 'bundle-a'))).toBe(false)
|
||||
} else {
|
||||
await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow(
|
||||
mode === 'null' || mode === 'null-subpath'
|
||||
? 'cannot resolve ESM export bundle-a'
|
||||
: 'resolves outside its package',
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('requires a package version before writing a packaged proxy', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
|
||||
const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
|
||||
manifest.version = ''
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow(
|
||||
'installed package bundle-a must declare a non-empty version',
|
||||
)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces plain-node links and stale managed proxies in packaged mode', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
expect(lstatSync(proxy).isSymbolicLink()).toBe(true)
|
||||
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(lstatSync(proxy).isDirectory()).toBe(true)
|
||||
const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
|
||||
version: string
|
||||
}
|
||||
stale.version = 'stale'
|
||||
writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale))
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({
|
||||
version: '0.0.0',
|
||||
})
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces a managed packaged proxy with a plain-node symlink', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(lstatSync(fallback).isDirectory()).toBe(true)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
|
||||
await healProfilesModuleFallback(anchor, home)
|
||||
expect(lstatSync(fallback).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects foreign packaged fallback directories with valid or invalid metadata', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
for (const metadata of ['{}', '{']) {
|
||||
const home = tmp()
|
||||
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
mkdirSync(proxy, { recursive: true })
|
||||
writeFileSync(join(proxy, 'package.json'), metadata)
|
||||
await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow(
|
||||
'exists and is not a dsh-managed module proxy',
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../util/atomic-write"
|
||||
},
|
||||
{
|
||||
"path": "../../util/launch-environment"
|
||||
},
|
||||
|
||||
@@ -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 packages/bundle/README.md
|
||||
README.md: d6b24a276fa64bb2eb80c2aad1783795e351ebc4
|
||||
README.zh.md: 36acc510cfab7979d28052ab26687dce58175155
|
||||
README.md: c0ea5be0c6f1b2457166b24a1718f6ab3aa0ffe6
|
||||
README.zh.md: 6c3520668d8f83cfbdc75652fac81f29d9bb810d
|
||||
|
||||
@@ -8,10 +8,11 @@ The manifest declaration, not this directory, defines Bundle identity. Domain pa
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`base/`](base/README.md) | The shared dsh core every profile applies first | — (patch only) |
|
||||
| [`base/`](base/README.md) | The shared dsh core applied first by base-backed profiles | — (patch only) |
|
||||
| [`acp-app/`](acp-app/README.md) | Automation-only ACP stdio application over base | mounts the ACP bridge |
|
||||
| [`web-app/`](web-app/README.md) | Browser surface: web patch layer + runtime glue plugin | mounts rows |
|
||||
| [`headless/`](headless/README.md) | Direct one-shot task mode over base, with no Host or Web layer | mounts `headless-runner` |
|
||||
| [`sdk-app/`](sdk-app/README.md) | SDK stdio JSON-RPC application over base | mounts the SDK server |
|
||||
| [`sdk-minimal/`](sdk-minimal/README.md) | Standalone minimal SDK application without base or Web | — (complete patch tree) |
|
||||
|
||||
In-box bundles resolve from the dsh installation; out-of-tree bundles install into a profile through `dsh plugin --profile <name> add <package>`.
|
||||
|
||||
@@ -8,10 +8,11 @@ Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
| [`base/`](base/README.zh.md) | 每个 profile 最先应用的共享 dsh 核心 | —(仅 patch) |
|
||||
| [`base/`](base/README.zh.md) | 基于 base 的 profile 最先应用的共享 dsh 核心 | —(仅 patch) |
|
||||
| [`acp-app/`](acp-app/README.zh.md) | 运行在 base 之上的 automation-only ACP stdio 应用 | 挂载 ACP bridge |
|
||||
| [`web-app/`](web-app/README.zh.md) | 浏览器表层:web patch 层 + 运行时粘合插件 | 挂载多条配置行 |
|
||||
| [`headless/`](headless/README.zh.md) | 直接运行在 base 之上的一次性任务模式,不含 Host 或 Web 层 | 挂载 `headless-runner` |
|
||||
| [`sdk-app/`](sdk-app/README.zh.md) | 运行在 base 之上的 SDK stdio JSON-RPC 应用 | 挂载 SDK server |
|
||||
| [`sdk-minimal/`](sdk-minimal/README.zh.md) | 不含 base 或 Web 的独立极简 SDK 应用 | 无(完整 patch 树) |
|
||||
|
||||
内置组合包从 dsh 安装目录解析;树外(out-of-tree)组合包通过 `dsh plugin --profile <name> add <package>` 安装进 profile。
|
||||
|
||||
@@ -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 packages/bundle/base/README.md
|
||||
README.md: 74f1288b46dd20643a494acb1829dbe38c367622
|
||||
README.zh.md: dda46a89f3c2b161d7358109e4317a976eaa65c8
|
||||
README.md: 9fb1264f39aee3f3961ff4fd3c07f35b0b7cefdf
|
||||
README.zh.md: 844254acc1f349a9debf38494e8d97f5f7e2d00b
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
|
||||
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of each base-backed profile's `dsh.profile.bundles` list. The standalone [`sdk-minimal`](../sdk-minimal/README.md) profile deliberately does not include this bundle. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
|
||||
|
||||
The base module-HMR row is disabled. A profile with a tested source-module reload lifecycle enables that row explicitly; `patchReload: live` config watching is independent and uses the launcher's watch-only fallback while module HMR remains disabled.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
|
||||
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.zh.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个基于 base 的 profile 的 `dsh.profile.bundles` 列表中的第一层。独立的 [`sdk-minimal`](../sdk-minimal/README.zh.md) profile 刻意不包含本组合包。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.zh.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.zh.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
|
||||
|
||||
base 的模块 HMR 配置项默认禁用。具有经过验证的源码模块重载生命周期的 profile 必须显式启用该配置项;`patchReload: live` 配置监视与之独立,在模块 HMR 保持禁用时使用启动器的仅监视 fallback。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# The dsh-base bundle patch: the shared core of every dsh profile, applied as
|
||||
# The dsh-base bundle patch: the shared core of each base-backed profile, applied as
|
||||
# ONE insert over the empty profile root. Later bundle patches and the user's
|
||||
# profile cordis.patch.yml address these rows by id, with the last write
|
||||
# winning per row.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-base",
|
||||
"description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root",
|
||||
"description": "The shared dsh core as a profile bundle: the first patch layer of base-backed profiles, inserting core rows over the empty profile root",
|
||||
"version": "0.1.1-rc.2",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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 packages/bundle/sdk-app/README.md
|
||||
README.md: 0356d6f4a99d7baef6ff7619d505392ff7f7f1d2
|
||||
README.zh.md: c70eb685954ebff42bca6c3d289ab58e46298d50
|
||||
README.md: c5022bd2096fae931fff48944bfc167280e62476
|
||||
README.zh.md: 687a19dc09679d11696a207ad17db3cf463ee901
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout.
|
||||
The SDK stdio application as a `dsh` profile bundle over [`dsh-base`](../base/README.md). It inherits the base's disabled module-HMR policy; its patch sets the coding-agent persona, mounts an app-owned zero-option command provider, and starts [`dsh-sdk-jsonrpc-server`](../../sdk/server/README.md) only after that provider accepts the invocation. `dsh --profile sdk --help` therefore writes help and exits without claiming stdin or stdout. The standalone [`sdk-minimal`](../sdk-minimal/README.md) bundle reuses the same startup provider and supplies its own profile name.
|
||||
|
||||
The startup provider binds stdin EOF to the launcher's bounded successful shutdown. SDK protocol `shutdown`, SIGINT, and SIGTERM retain their owning server or launcher paths; disposal drains the root profile tree and persistence. Stdout is reserved for newline-delimited JSON-RPC frames. The bundle disables model-generated session titles because the SDK exposes no title surface; deterministic fallback titles remain durable without an auxiliary model request. A deployment selects a different complete composition through profile bundles and patch files, not another app bin.
|
||||
|
||||
| Config | Default | Behavior |
|
||||
|---|---|---|
|
||||
| `profile` | `sdk` | Profile name rendered in command help; a bundle mounting this provider sets its own shipped profile name. |
|
||||
|
||||
`DSH_MAX_TOKENS_AS_SUCCESS` retains the SDK deployment mapping: unset or JSON `true` reports token-limited subagent completion as accepted, while JSON `false` reports it as an error. Provider/model and workspace cwd arrive through the SDK initialization request; the base profile owns adapters, tools, persistence, policy, settings, and credentials.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user