mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge remote-tracking branch 'origin/master' into fix/open-file-fail
This commit is contained in:
@@ -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: 94e3ddad0cfea0a1aaf2efe8d15f26f471bb2bd3
|
||||
2026-08-05-profile-plugin-bundles.zh.md: 96d22c57f68ecc72e1afd34fbbd3cfbe3ac24c61
|
||||
2026-08-05-profile-plugin-bundles.md: c9f685eecddcd4ea8d8580becef3c32a9693a329
|
||||
2026-08-05-profile-plugin-bundles.zh.md: 43680862a13171071e236d56c336b790bbe50432
|
||||
|
||||
@@ -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 shipped bundles are `@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` (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.
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -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.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。
|
||||
|
||||
随附的组合包是 `@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.md)负责 headless 组合约定。
|
||||
默认 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.md)负责 headless 组合约定。
|
||||
|
||||
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
|
||||
|
||||
|
||||
+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-10-product-subagent-providers-in-shared-host.md
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: 8bc08ddb57f07b76d3f90ce7c375e79c666ce86d
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: f3d2053c78f52deda5130eee908e7c2eff98b89a
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: 196e28c1263c4b6d71eaeb59b9ba8457b36f3ff4
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: b36398be50065dd1520fb97ca15416467c580bf2
|
||||
|
||||
+6
-8
@@ -8,21 +8,19 @@ English | [中文](2026-08-10-product-subagent-providers-in-shared-host.zh.md)
|
||||
|
||||
The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) were first shipped as independently installable packages that a deployment loaded beside the common subagent tool. Agent Presets later became the ordinary owner of one agent's model-visible tools, but a preset cannot safely own these product providers: `ctx.subagents` is a process registry, provider names are unique within the Host, and host consumers resolve the same registry across sessions. Repeated preset composition would therefore contend for the same configured names. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
|
||||
|
||||
The placement decision must preserve two independent facts. Loading a provider must not start or authenticate a product, while enabling a tool must remain per preset so two sessions can expose different products. A global product switch, a provider instance per agent, or pre-enumerated combination presets would each create a second owner for one of those facts.
|
||||
The placement decision must preserve two independent facts. Loading a provider must not start or authenticate a product, while granting a tool must remain per preset so two sessions can expose different products. A global product switch, a provider instance per agent, or pre-enumerated combination presets would each create a second owner for one of those facts.
|
||||
|
||||
## Decision
|
||||
|
||||
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider package and mounts the required instances on the host plane. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns each row's registry identity: both products accept multiple unique `providerName` values while preserving `codex` and `claude-code` as their defaults. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows whose `provider` and `toolName` values expose exactly the configured instances needed by one agent without changing the Host registry.
|
||||
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider Bundle; its patch mounts the default instance, and the Profile may mount additional named instances on the host plane. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns each row's registry identity: both products accept multiple unique `providerName` values while preserving `codex` and `claude-code` as their defaults. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows whose `provider` and `toolName` values expose exactly the configured instances needed by one agent without changing the Host registry.
|
||||
|
||||
This note continues to own why a mounted product provider belongs on the host plane while its model-facing tool belongs to an Agent Preset. The production-install exclusion decision owns which Profiles install those optional packages. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
|
||||
Each provider package owns its directly installable Bundle patch and private product runtime. This note continues to own process-wide Host placement whenever either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
|
||||
|
||||
The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, or test authentication. It may supply each mounted Provider instance's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing commands and product failures remain local to the attempted delegation.
|
||||
|
||||
Only a Profile that selects the Claude Code provider carries the Claude Agent SDK's optional platform CLI payload. Production still resolves the host `claude`; the SDK payload remains provider-package installation cost rather than the production executable.
|
||||
Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its pinned Agent SDK select the private native executable. Neither provider consults or falls back to a host product command. Profile loading creates no product state, probes no version or authentication, and may supply each mounted Provider instance's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing platform payloads and product failures remain local to the attempted delegation.
|
||||
|
||||
## Verification
|
||||
|
||||
The base bundle test proves production `dsh-base` contains neither product provider dependency nor provider row. The Web composition explicitly mounts both optional providers and covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Package-owned Loader compositions prove two named instances of each product register without starting a product process. Keyless ACP snapshots pin the Codex two-tool roster and the final four-tool combination, while provider tests separately prove native executable resolution, configuration isolation, failure, cancellation, and process-tree quiescence.
|
||||
The base bundle test proves production `dsh-base` contains neither product provider dependency nor provider row. The Web composition installs both optional Bundles and covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Package-owned Loader compositions prove each Bundle default and additional named instances register without starting a product process. Keyless ACP snapshots pin the Codex two-tool roster and the final four-tool combination, while provider tests separately prove private platform-payload selection without host fallback, configuration isolation, failure, cancellation, and process-tree quiescence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -38,4 +36,4 @@ The base bundle test proves production `dsh-base` contains neither product provi
|
||||
|
||||
A user installs each selected product provider in a Profile, mounts the required named instances, and exposes their tools through the same Agent Preset authoring path as other plugins. Each new session receives exactly the tools its chosen preset contributes. Profiles that do not select a product provider carry no corresponding package or module-loading footprint; loading selected instances still starts no product process, login, model call, or product home.
|
||||
|
||||
The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is a two-layer opt-in: the Profile owns installation and host-plane registration, while the Preset owns per-agent exposure. Selecting the Claude provider also accepts its current SDK optional-payload installation cost.
|
||||
The Host registry remains the single provider authority, each Bundle remains the deployment availability authority, and each Preset remains the model-tool authority. This explicit two-gate lifecycle avoids a global enable switch and keeps package removal independent from per-session authoring.
|
||||
|
||||
+6
-8
@@ -8,21 +8,19 @@ Status: implemented
|
||||
|
||||
[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称在 Host 内唯一,而宿主消费方会跨会话解析同一个注册表。因此,重复组装 preset 会争用同一组已配置名称。如果要求用户同时编辑 Profile 和 Preset,也会使通用 preset 配置项本身不完整。
|
||||
|
||||
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具是否启用仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
|
||||
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具授权仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
|
||||
|
||||
## 决策
|
||||
|
||||
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方包,并在 host plane 挂载所需实例。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.md)负责每个配置项的注册身份:两个产品都接受多个唯一的 `providerName`,同时保留 `codex` 与 `claude-code` 作为默认值。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 通过普通 `dsh-tool-subagent` 配置项的 `provider` 与 `toolName` 准确公开单个 agent 所需的已配置实例,而无需更改 Host 注册表。
|
||||
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方 Bundle;其 patch 挂载默认实例,而 Profile 可以在 host plane 挂载更多命名实例。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.md)负责每个配置项的注册身份:两个产品都接受多个唯一的 `providerName`,同时保留 `codex` 与 `claude-code` 作为默认值。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 通过普通 `dsh-tool-subagent` 配置项的 `provider` 与 `toolName` 准确公开单个 agent 所需的已配置实例,而无需更改 Host 注册表。
|
||||
|
||||
本说明继续负责解释为什么已经挂载的产品提供方属于 host plane,而面向模型的工具属于 Agent Preset。生产安装排除决策负责哪些 Profile 安装这些可选包。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
|
||||
每个提供方包都拥有可直接安装的 Bundle patch 与私有产品运行时。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
|
||||
|
||||
这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本或测试身份验证。它可以提供每个已挂载 Provider 实例的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。命令缺失和产品故障仍局限于发生问题的那次委派。
|
||||
|
||||
只有选择 Claude Code 提供方的 Profile 才会携带 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷。生产环境仍解析宿主提供的 `claude`;这份 SDK 载荷是提供方包的安装成本,而不是生产可执行文件。
|
||||
每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapper,Claude Code 包则让锁定的 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令。加载 Profile 不会创建产品状态、探测版本或测试身份验证;它可以提供每个已挂载 Provider 实例的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。平台载荷缺失和产品故障仍局限于发生问题的那次委派。
|
||||
|
||||
## 验证
|
||||
|
||||
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置项。Web 组装显式挂载两个可选提供方,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明每个产品的两个命名实例都会完成注册,而不会启动产品进程。无密钥 ACP(Agent Client Protocol)快照固定 Codex 双工具集合与最终四工具组合,提供方测试则另行证明原生可执行文件解析、配置隔离、失败、取消和进程树完全停稳。
|
||||
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置项。Web 组装会安装两个可选 Bundle,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明每个 Bundle 默认实例与额外命名实例都会完成注册,而不会启动产品进程。无密钥 ACP(Agent Client Protocol)快照固定 Codex 双工具集合与最终四工具组合,提供方测试则另行证明私有平台载荷选择与无宿主回退、配置隔离、失败、取消和进程树完全停稳。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -38,4 +36,4 @@ base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,
|
||||
|
||||
用户在 Profile 中安装每个被选中的产品提供方,挂载所需命名实例,再通过与其他插件相同的 Agent Preset 创作路径公开这些实例的工具。每个新会话只会获得其所选 preset 所贡献的工具。没有选择产品提供方的 Profile 不承担对应包或模块的加载开销;加载已选择的实例仍不会启动产品进程、登录、调用模型或创建产品主目录。
|
||||
|
||||
宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是两层按需启用:Profile 负责安装与 host plane 注册,Preset 负责按 agent 暴露。选择 Claude 提供方还会接受当前 SDK 可选载荷的安装成本。
|
||||
Host 注册表仍是提供方的唯一权威,每个 Bundle 仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
|
||||
|
||||
+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-04-claude-code-and-codex-subagent-backends.md
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: b478a97d78cc7aaa9dad452bc5cd4cbb5fdf361e
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 761fe5df5a87a2c87af2e8a8dd6cb593af0d5ba2
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: a8f500c7fb934b8634456e1618498909f682f0e2
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: ca35c38617b1ca38757959735b11618559f6f804
|
||||
|
||||
+6
-6
@@ -12,7 +12,7 @@ The product integrations must not become second owners for task text, cwd, cance
|
||||
|
||||
## Decision
|
||||
|
||||
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity and static tool binding, the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and diagnostic production. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
|
||||
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity and static tool binding, the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and diagnostic production. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
|
||||
|
||||
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
|
||||
|
||||
@@ -34,7 +34,7 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
|
||||
|
||||
## Codex provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex` and starts `codex app-server --stdio` from `PATH`. Its public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. Installation, login, `CODEX_HOME`, model selection, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
|
||||
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. Installation, login, `CODEX_HOME`, model selection, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
|
||||
|
||||
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, maps the resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no mode or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
|
||||
|
||||
@@ -48,7 +48,7 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
|
||||
|
||||
## Claude Code provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` executable name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.220 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
|
||||
The public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
|
||||
|
||||
@@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
|
||||
|
||||
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
|
||||
|
||||
The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended command rejection with safe diagnostic and no file side effect, explicit dangerous-bypass writing in suite-owned temporary storage, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
|
||||
The Codex evidence pins `@openai/codex@0.147.0`, `codex-cli 0.147.0`, and all six optional platform aliases. Its real-product spec observes the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended command rejection with safe diagnostic and no file side effect, explicit dangerous-bypass writing in suite-owned temporary storage, local cancellation, wrapper/native whole-tree exit, and missing-payload failure without host fallback.
|
||||
|
||||
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
|
||||
|
||||
The Claude Code evidence pins Agent SDK 0.3.220 and uses its platform-distributed Claude Code 2.1.220 CLI as the deterministic compatibility fixture, routed through the same native executable-resolution path production uses. Its real-product spec observes the exact `x-api-key`, original task, byte-exact final answer, an inherited interactive host setting overridden by the safe Provider mode, denied and bypassed writes in suite-owned temporary directories, safe permission diagnostics, process failure, local cancellation, whole-tree exit, and a real Windows batch shim under a path containing percent, ampersand, and exclamation metacharacters. This evidence proves the official SDK/CLI integration path, not compatibility with every independently installed product version. The Loader and shipped-profile evidence resolve both product packages by name while starting neither product, and the provider suite proves that the SDK receives the executable resolved from the host `PATH`.
|
||||
The Claude Code evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, an inherited interactive host setting overridden by the safe Provider mode, denied and bypassed writes in suite-owned temporary directories, safe permission diagnostics, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
|
||||
|
||||
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
|
||||
|
||||
@@ -90,6 +90,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
|
||||
|
||||
Users delegate through Profile-configured one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); named instance identity and tool binding are owned by the [named-instance decision](2026-08-18-product-subagent-named-instances.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
|
||||
|
||||
Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, workspace settings, and selected Provider mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Both products use Bundle-pinned platform CLIs plus native account and workspace settings and the selected Provider permission mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
|
||||
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
|
||||
|
||||
+6
-6
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.md)负责 Profile 选择的提供方身份与静态工具绑定,[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责各产品提供方的 Profile 模式选择与诊断生产。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
|
||||
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.md)负责 Profile 选择的提供方身份与静态工具绑定,[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责各自独立的可选 Bundle 与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责各产品提供方的 Profile 模式选择与诊断生产。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
|
||||
|
||||
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs` 和 `dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
|
||||
|
||||
@@ -34,7 +34,7 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
|
||||
|
||||
## Codex 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,并启动 `codex app-server --stdio`,该命令从 `PATH` 解析。其公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。安装、登录、`CODEX_HOME`、模型选择、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
|
||||
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.147.0` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。安装、登录、`CODEX_HOME`、模型选择、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approval/reviewer/sandbox 字段。
|
||||
|
||||
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,把已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
|
||||
|
||||
@@ -48,7 +48,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
|
||||
|
||||
## Claude Code 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定的 `claude` 可执行文件名称,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.220 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
|
||||
公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
|
||||
|
||||
@@ -62,11 +62,11 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
|
||||
|
||||
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Codex Loader fixture 会公开两个命名 Codex 实例与工具;Claude Code Loader fixture 会公开默认 Codex 工具以及两个命名 Claude Code 实例与工具。两个 fixture 都包含通用 Job 控制工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
|
||||
|
||||
Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、带安全诊断且不产生文件副作用的无人值守命令拒绝、测试拥有临时存储中的显式危险绕过写入、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
|
||||
Codex 证据会锁定 `@openai/codex@0.147.0`、`codex-cli 0.147.0` 与六个平台 alias。其真实产品测试会观测包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、带安全诊断且不产生文件副作用的无人值守命令拒绝、测试拥有临时存储中的显式危险绕过写入、本地取消、wrapper/原生整棵进程树退出,以及载荷缺失时不回退宿主命令的失败。
|
||||
|
||||
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
|
||||
|
||||
Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、安全提供方模式对继承的交互式宿主设置的覆盖、测试所拥有临时目录中的拒绝写入与 bypass 写入、安全权限诊断、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
|
||||
Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、安全提供方模式对继承的交互式宿主设置的覆盖、测试所拥有临时目录中的拒绝写入与 bypass 写入、安全权限诊断、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
|
||||
|
||||
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
|
||||
|
||||
@@ -90,6 +90,6 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
|
||||
|
||||
用户通过由 Profile 配置、并由官方产品集成支持的一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;命名实例身份与工具绑定由[命名实例决策](2026-08-18-product-subagent-named-instances.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
|
||||
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态、工作区设置和所选提供方模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。两个产品都使用 Bundle 锁定的平台 CLI,并保留原生账户与工作区设置以及所选提供方权限模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
|
||||
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
|
||||
|
||||
+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/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.md: 3e3e4fbefb31932a637bfe05ff0d90916e202a79
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 166964675bf7084b62f5500969e5756c9bd9f644
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.md: faa14a5848a421389d8c427f844c4dedc118a0fd
|
||||
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 33be6118b7e5de261d22e62b1df98e1afad7a08c
|
||||
|
||||
+8
-4
@@ -6,20 +6,24 @@ English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers
|
||||
|
||||
## Problem
|
||||
|
||||
`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code, including the Claude Agent SDK, even when neither integration is used.
|
||||
`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code and large platform CLI payloads, even when neither integration is used.
|
||||
|
||||
## Decision
|
||||
|
||||
This decision supersedes the [shared-host placement](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md): `@deepseek-ai/dsh-base` does not depend on or mount the Codex and Claude Code subagent providers. Their packages remain available for Profiles that install and mount them explicitly. Repository examples keep direct development dependencies so their explicit provider configurations continue to resolve.
|
||||
This decision partially supersedes only the default-inclusion part of the [shared-host placement](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md): `@deepseek-ai/dsh-base` does not depend on or mount the Codex and Claude Code subagent providers. Each provider package is a directly installable Profile Bundle whose `dsh.bundle.patch` points to one package-owned `cordis.patch.yml`. Each patch contributes exactly one self-provider Host row and no Agent tool row.
|
||||
|
||||
The two Bundles remain independent. The Codex Bundle owns the pinned official wrapper and six platform aliases; production starts the package-declared wrapper and never falls back to a host `codex`. The Claude Code Bundle owns the pinned Agent SDK and matching platform CLI; production lets the SDK select that private CLI and never falls back to a host `claude`. Installing one Bundle does not pull in the other, and the default `@deepseek-ai/dsh` production closure contains neither provider nor either product runtime. Each installed Bundle registers a dormant provider on the next Profile start, while an Agent Preset independently decides whether a new Session receives the corresponding tool. Installation does not start a product, authenticate an account, rewrite native settings, or grant model access.
|
||||
|
||||
## Verification
|
||||
|
||||
The base bundle test rejects both provider dependencies and configuration rows. Cordis configuration validation requires explicit examples to declare the provider packages they name.
|
||||
Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.147.0, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep dormant providers in the base bundle.** Dormant providers start no product processes, but their packages still enter every production npm install.
|
||||
|
||||
**Add a wrapper or meta Bundle.** A third package would duplicate installation ownership and make independent removal less direct without contributing another runtime capability.
|
||||
|
||||
## Consequences
|
||||
|
||||
Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. Using either integration requires explicit Profile configuration.
|
||||
Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. A Profile can add or remove either provider Bundle independently; changed Host availability takes effect on the next Profile start, and selecting a product explicitly accepts its private platform payload. A separately authored Agent Preset grants either model-visible tool only to newly composed Sessions. No wrapper package beyond the products' official distributions, meta Bundle, dynamic installer, or persisted product-enable state is introduced.
|
||||
|
||||
+8
-4
@@ -6,20 +6,24 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码,包括 Claude Agent SDK,即使用户并未使用任一集成。
|
||||
`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码与大型平台 CLI 载荷,即使用户并未使用任一集成。
|
||||
|
||||
## 决策
|
||||
|
||||
本决策取代[共享 host 放置决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md):`@deepseek-ai/dsh-base` 不依赖也不挂载 Codex 与 Claude Code subagent 提供方。需要这些集成的 Profile 仍可显式安装并挂载对应包。仓库 examples 保留直接开发依赖,使其显式提供方配置可以继续解析。
|
||||
本决策只部分取代[共享 host 放置决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)中关于默认包含提供方的部分:`@deepseek-ai/dsh-base` 不依赖也不挂载 Codex 与 Claude Code subagent 提供方。每个提供方包都是可直接安装的 Profile Bundle,其 `dsh.bundle.patch` 指向包自身拥有的 `cordis.patch.yml`。每份 patch 恰好贡献一条挂载自身提供方的 Host 行,不包含 Agent 工具行。
|
||||
|
||||
两个 Bundle 彼此独立。Codex Bundle 自己负责锁定的官方 wrapper 与六个平台 alias;生产环境会启动包所声明的 wrapper,绝不会回退到宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK 与匹配平台 CLI;生产环境让 SDK 选择该私有 CLI,绝不会回退到宿主 `claude`。安装其中一个 Bundle 不会带入另一个,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含任一产品运行时。每个已安装 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装不会启动产品、验证账户、改写原生设置或向模型授予访问权。
|
||||
|
||||
## 验证
|
||||
|
||||
base 组合包测试会拒绝这两个提供方依赖与配置行。Cordis 配置验证要求显式 examples 声明其引用的提供方包。
|
||||
包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.147.0、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**在 base 组合包中保留休眠提供方。** 休眠提供方不会启动产品进程,但其包仍会进入每次生产 NPM 安装。
|
||||
|
||||
**新增 wrapper 或 meta Bundle。** 第三个包会重复安装责任,使独立移除变得更间接,却不会贡献新的运行时能力。
|
||||
|
||||
## 后果
|
||||
|
||||
安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。使用任一集成都需要显式 Profile 配置。
|
||||
安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以独立添加或移除任一 provider Bundle;Host 可用性的变化会在下次 Profile 启动时生效,选择产品也代表明确接受其私有平台载荷。单独创作的 Agent Preset 仍只会向新组装的 Session 授予任一模型可见工具。本决策不会在产品官方发行版之外引入 wrapper 包,也不引入 meta Bundle、动态安装程序或持久化的产品启用状态。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
|
||||
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python closure is recorded separately in [`python/sdk/uv.lock`](python/sdk/uv.lock).
|
||||
|
||||
@@ -39,6 +39,7 @@ External packages that a workspace package resolves at runtime. The tier covers
|
||||
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
|
||||
| [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT |
|
||||
| [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
|
||||
| [`@openai/codex`](https://github.com/openai/codex) | Apache-2.0 |
|
||||
| [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
| [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
|
||||
@@ -122,7 +123,6 @@ External packages **directly declared** only by repository tooling, test infrast
|
||||
| [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
|
||||
| [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
|
||||
| [`@openai/codex`](https://github.com/openai/codex) | Apache-2.0 |
|
||||
| [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT |
|
||||
| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
|
||||
| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
|
||||
|
||||
@@ -198,9 +198,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -185,9 +185,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -123,7 +123,16 @@ After a clean mount-validation, ask the user to start a session on the new prese
|
||||
|
||||
## Native product subagents
|
||||
|
||||
Codex and Claude Code providers belong on the host plane but are not installed by production `dsh`. The active Profile must install and mount the selected provider before a preset can expose its ordinary delegation-tool row; never move a product provider into the preset and never add a product-specific settings field.
|
||||
Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start.
|
||||
|
||||
Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
|
||||
|
||||
@@ -149,7 +158,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
|
||||
|
||||
For additional named Codex or Claude Code instances, mount a separate host-plane provider row for each instance with a unique `providerName`, then add a separate preset tool row whose `provider` exactly matches that name and whose `toolName` is also unique. Keep the shipped rows for the default `codex` and `claude-code` names; do not reuse one tool row for several providers or derive either name from permission or environment settings.
|
||||
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install or mount either optional provider: before enabling a row, the Profile must install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` package and mount the required provider instances on the host plane. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. The host must also provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product.
|
||||
The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. Production `dsh` does not install either optional provider: before enabling a row, install the matching `@deepseek-ai/dsh-subagent-codex` or `@deepseek-ai/dsh-subagent-claude-code` Bundle in the Profile and restart it. Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI; additional named instances use extra host-plane rows from the same installed package. A preset cannot provide that host dependency. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Installing a Bundle or composing a preset row does not start a product, authenticate an account, select a model, probe credentials, or manage native product settings.
|
||||
|
||||
## What not to move into a preset
|
||||
|
||||
|
||||
@@ -197,9 +197,10 @@
|
||||
toolName: subagent_fork
|
||||
backgroundMode: continuable
|
||||
|
||||
# Production dsh does not install these optional providers. An opting-in
|
||||
# Profile mounts each provider once on the host plane; copy this preset,
|
||||
# then remove `disabled` from the matching tool row.
|
||||
# Production dsh does not install these optional providers. Install the
|
||||
# matching Bundle in this Profile and restart the Host, then copy this
|
||||
# preset and remove `disabled` from the matching tool row. Host availability
|
||||
# alone grants no tool.
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
|
||||
@@ -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: 0be64fdfc0ad4e81d23f25a26881fa89f37565b0
|
||||
README.zh.md: 649bf15df814abf4875fed794c2e76c494bfcc25
|
||||
README.md: 7828f55a2e4adfd85a0018baada6945ea75aacb0
|
||||
README.zh.md: e14e13731c314efd4d39913b91f2e90ba624e55c
|
||||
|
||||
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
|
||||
|
||||
`dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack.
|
||||
|
||||
The Codex and Claude Code subagent providers are separate optional Bundles. Add either package, both in one command, or remove either package independently:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, each installed Bundle registers only its dormant Host provider; a copied Preset must separately enable the matching tool row for new Agents. The [Codex provider README](../../../packages/subagent/subagent-codex/README.md) and [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) own executable, authentication, payload, and failure details; the [base Bundle reference](../../../packages/bundle/base/README.md) owns the default dependency closure.
|
||||
|
||||
```sh
|
||||
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
|
||||
dsh plugin --profile tui remove turtle-ui
|
||||
|
||||
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
|
||||
|
||||
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
|
||||
|
||||
Codex 与 Claude Code subagent provider 是两个彼此独立的可选 Bundle。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
```
|
||||
|
||||
pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装 Bundle 只注册自己的休眠 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.md)与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)负责可执行文件、身份验证、载荷与失败细节;[base Bundle 参考](../../../packages/bundle/base/README.md)负责默认依赖闭包。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
|
||||
dsh plugin --profile tui remove turtle-ui
|
||||
|
||||
@@ -650,6 +650,21 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
}
|
||||
expect(Object.keys(manifest.dependencies)).toEqual(['anchored-bundle'])
|
||||
expect(manifest.dsh.profile.bundles).toContain('anchored-bundle')
|
||||
|
||||
const removed = await runBuiltBin(
|
||||
['plugin', '--profile', 'anchor', 'remove', 'anchored-bundle'],
|
||||
{ DSH_HOME: home },
|
||||
checkout,
|
||||
)
|
||||
expect(removed.code).toBe(0)
|
||||
const afterRemove = JSON.parse(
|
||||
readFileSync(join(home, 'profiles', 'anchor', 'package.json'), 'utf8'),
|
||||
) as {
|
||||
dependencies?: Record<string, string>
|
||||
dsh: { profile: { bundles: string[] } }
|
||||
}
|
||||
expect(Object.keys(afterRemove.dependencies ?? {})).toEqual([])
|
||||
expect(afterRemove.dsh.profile.bundles).not.toContain('anchored-bundle')
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
rmSync(checkout, { recursive: true, force: true })
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
|
||||
import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets'
|
||||
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
|
||||
@@ -26,9 +26,10 @@ const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
/** The shipped Web surface: the dsh-base and dsh-web-app bundle patches over an empty preset root. */
|
||||
const BASE_PATCH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
|
||||
const WEB_PATCH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
|
||||
const CODEX_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-codex')
|
||||
const CLAUDE_CODE_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-claude-code')
|
||||
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
|
||||
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
|
||||
const EXAMPLES_INSTALL_ANCHOR = join(REPO_ROOT, 'examples/package.json')
|
||||
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
|
||||
const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
||||
@@ -47,12 +48,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
|
||||
async function bootWeb(
|
||||
settingsFile: string,
|
||||
extra: PatchOptions[] = [],
|
||||
extraInstallAnchor?: string,
|
||||
profilePackages: readonly string[] = [],
|
||||
profileBundles?: readonly string[],
|
||||
): Promise<Context> {
|
||||
const storageRoot = join(dirname(settingsFile), 'storages')
|
||||
const patches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
...loadOverlayPatches('dsh-test', WEB_PATCH),
|
||||
const overrides: PatchOptions[] = [
|
||||
// The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
|
||||
// reads the developer's own document — and since the default preset is a
|
||||
// setting, a stored `agent-presets.default` would decide this file's
|
||||
@@ -115,12 +115,34 @@ async function bootWeb(
|
||||
// them resolvable — the same mechanism, not a test-only shim.
|
||||
const home = dirname(settingsFile)
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR, home)
|
||||
if (extraInstallAnchor !== undefined) healProfilesModuleFallback(extraInstallAnchor, home)
|
||||
const profileDir = join(home, 'profiles', 'spec')
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
// Product Bundles are installed into the Profile, not the dsh app. Model
|
||||
// pnpm's package link for only the selected products; their own production
|
||||
// dependencies resolve from the linked workspace packages, while shared
|
||||
// peers still resolve through the installation fallback above.
|
||||
for (const packageDir of profilePackages) {
|
||||
const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8')) as { name: string }
|
||||
const link = join(profileDir, 'node_modules', manifest.name)
|
||||
await mkdir(dirname(link), { recursive: true })
|
||||
await symlink(packageDir, link, 'junction')
|
||||
}
|
||||
let bundlePatches: PatchOptions[] = [
|
||||
...loadOverlayPatches('dsh-test', BASE_PATCH),
|
||||
...loadOverlayPatches('dsh-test', WEB_PATCH),
|
||||
]
|
||||
if (profileBundles !== undefined) {
|
||||
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
|
||||
private: true,
|
||||
dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
|
||||
dsh: { profile: { bundles: profileBundles } },
|
||||
}, null, 2) + '\n')
|
||||
const profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
|
||||
bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
}
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
await writeFile(rootConfig, '[]\n')
|
||||
return await boot('dsh-test', rootConfig, patches, (bootCtx) => {
|
||||
return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], (bootCtx) => {
|
||||
provideCmdline(bootCtx, { args: [], exit: () => {} })
|
||||
})
|
||||
}
|
||||
@@ -434,17 +456,18 @@ describe('the shipped Web composition', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('product subagent rows in user presets', () => {
|
||||
let productCtx: Context
|
||||
const ids = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
|
||||
describe('product Bundle and user-preset intersection', () => {
|
||||
const presetIds = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
|
||||
type Product = 'codex' | 'claude-code'
|
||||
type PresetId = typeof presetIds[number]
|
||||
|
||||
beforeAll(async () => {
|
||||
async function bootProducts(installed: readonly Product[]): Promise<Context> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
|
||||
const userRoot = join(root, 'presets')
|
||||
const settingsFile = join(root, 'settings.yaml')
|
||||
const standard = await readFile(join(CONFIG_DIR, 'agent-presets', 'standard', 'agent.cordis.yml'), 'utf8')
|
||||
await writeFile(settingsFile, '{}\n')
|
||||
for (const id of ids) {
|
||||
for (const id of presetIds) {
|
||||
let composition = standard
|
||||
if (id === 'products-codex' || id === 'products-both') {
|
||||
composition = enablePresetTool(composition, 'tool-subagent-codex')
|
||||
@@ -456,11 +479,15 @@ describe('product subagent rows in user presets', () => {
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'agent.cordis.yml'), composition)
|
||||
}
|
||||
productCtx = await bootWeb(settingsFile, [
|
||||
{ insert: [
|
||||
{ id: 'subagent-codex', name: '@deepseek-ai/dsh-subagent-codex' },
|
||||
{ id: 'subagent-claude-code', name: '@deepseek-ai/dsh-subagent-claude-code' },
|
||||
] },
|
||||
const packageDir = (product: Product): string => (
|
||||
product === 'codex' ? CODEX_PACKAGE_DIR : CLAUDE_CODE_PACKAGE_DIR
|
||||
)
|
||||
const packageName = (product: Product): string => (
|
||||
product === 'codex'
|
||||
? '@deepseek-ai/dsh-subagent-codex'
|
||||
: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
)
|
||||
return await bootWeb(settingsFile, [
|
||||
{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
@@ -472,46 +499,68 @@ describe('product subagent rows in user presets', () => {
|
||||
includeUserRoot: false,
|
||||
},
|
||||
},
|
||||
], EXAMPLES_INSTALL_ANCHOR)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await productCtx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('composes none, either product, or both without changing the shared host registry', async () => {
|
||||
const expected = new Map<string, string[]>([
|
||||
['products-none', []],
|
||||
['products-codex', ['subagent_codex']],
|
||||
['products-claude', ['subagent_claude_code']],
|
||||
['products-both', ['subagent_claude_code', 'subagent_codex']],
|
||||
], installed.map(packageDir), [
|
||||
'@deepseek-ai/dsh-base',
|
||||
'@deepseek-ai/dsh-web-app',
|
||||
...installed.map(packageName),
|
||||
])
|
||||
expect(productCtx.subagents.list()).toEqual(expect.arrayContaining([
|
||||
'spawn', 'fork', 'codex', 'claude-code',
|
||||
]))
|
||||
}
|
||||
|
||||
for (const [id, productTools] of expected) {
|
||||
const handle = await productCtx.agents.create({
|
||||
sessionId: SessionId(`preset-${id}`),
|
||||
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
|
||||
})
|
||||
it('composes the intersection of installed Bundles and enabled preset rows', async () => {
|
||||
const enabledByPreset: Record<PresetId, Product[]> = {
|
||||
'products-none': [],
|
||||
'products-codex': ['codex'],
|
||||
'products-claude': ['claude-code'],
|
||||
'products-both': ['codex', 'claude-code'],
|
||||
}
|
||||
const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
|
||||
{ installed: [], presets: ['products-both'] },
|
||||
{ installed: ['codex'], presets: ['products-both'] },
|
||||
{ installed: ['claude-code'], presets: ['products-both'] },
|
||||
{ installed: ['codex', 'claude-code'], presets: presetIds },
|
||||
]
|
||||
|
||||
for (const { installed, presets } of scenarios) {
|
||||
const productCtx = await bootProducts(installed)
|
||||
const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
|
||||
try {
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output']))
|
||||
for (const productTool of productTools) {
|
||||
expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([
|
||||
'description', 'prompt', 'run_in_background',
|
||||
])
|
||||
expect(productCtx.subagents.list()
|
||||
.filter(name => name === 'codex' || name === 'claude-code')
|
||||
.sort())
|
||||
.toEqual([...installed].sort())
|
||||
for (const id of presets) {
|
||||
const handle = await productCtx.agents.create({
|
||||
sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
|
||||
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
|
||||
})
|
||||
try {
|
||||
const productTools = enabledByPreset[id]
|
||||
.filter(product => installed.includes(product))
|
||||
.map(product => product === 'codex' ? 'subagent_codex' : 'subagent_claude_code')
|
||||
.sort()
|
||||
const tools = toolNames(productCtx, handle.agent)
|
||||
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
|
||||
.toEqual(productTools)
|
||||
expect(tools).toEqual(expect.arrayContaining(['job_kill', 'job_list', 'job_output']))
|
||||
for (const productTool of productTools) {
|
||||
expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([
|
||||
'description', 'prompt', 'run_in_background',
|
||||
])
|
||||
}
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
}
|
||||
expect(spawn).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
spawn.mockRestore()
|
||||
await productCtx.fiber.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 120_000)
|
||||
|
||||
it('applies a product-row edit only to later sessions on the preset', async () => {
|
||||
const productCtx = await bootProducts(['codex'])
|
||||
const preset = await productCtx.agentPresets.resolve('products-none')
|
||||
const original = await readFile(preset.path, 'utf8')
|
||||
const existing = await productCtx.agents.create({
|
||||
@@ -535,8 +584,9 @@ describe('product subagent rows in user presets', () => {
|
||||
} finally {
|
||||
await existing.dispose()
|
||||
await writeFile(preset.path, original)
|
||||
await productCtx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
}, 120_000)
|
||||
})
|
||||
|
||||
describe('a switch survives the session', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/skill-tool-row', import.
|
||||
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/skill-tool-row/ui.expected.md', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'skill-tool-row-web-e2e'
|
||||
const PROMPT = 'Load the snapshot-skill skill with the skill tool, then reply DONE.'
|
||||
const PROMPT = 'Load the editing-cordis-compositions skill with the skill tool, then reply DONE.'
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
|
||||
let scaffold: WebScaffold
|
||||
@@ -53,17 +53,17 @@ describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
|
||||
it('expands the loaded skill to its exact recorded instructions', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-skill-tool-row'))
|
||||
const call = page.locator('[data-tool="skill"]')
|
||||
const row = call.getByRole('button', { name: 'Skill snapshot-skill' })
|
||||
const row = call.getByRole('button', { name: 'Skill editing-cordis-compositions' })
|
||||
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(await call.getByText('snapshot-skill', { exact: true }).count()).toBe(1)
|
||||
expect(await call.getByText('editing-cordis-compositions', { exact: true }).count()).toBe(1)
|
||||
|
||||
await row.click()
|
||||
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('true')
|
||||
await call.getByText('Instructions', { exact: true }).waitFor()
|
||||
const output = call.locator('pre')
|
||||
await output.waitFor()
|
||||
expect(await output.textContent()).toContain('<skill_content name="snapshot-skill">')
|
||||
expect(await output.textContent()).toContain('Follow these snapshot-only instructions.')
|
||||
expect(await output.textContent()).toContain('<skill_content name="editing-cordis-compositions">')
|
||||
expect(await output.textContent()).toContain('Each Bundle registers its dormant default provider and exclusively uses its pinned package-local platform CLI')
|
||||
expect(await output.evaluate(element => getComputedStyle(element.parentElement!).maxHeight)).toBe('260px')
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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: acc9af96bcb004acc3ce780294797df31587a6d6
|
||||
module-graph.zh.md: 0347a6f48c52e74b371d3ab7d94045dee23c4db7
|
||||
module-graph.md: 54aa13217a01ed44b44925365526438d4c867928
|
||||
module-graph.zh.md: 33c2f53afeb94c6d844f8406c1043d780436f588
|
||||
|
||||
@@ -962,6 +962,12 @@ flowchart TD
|
||||
pkg_subagent_claude_code --> pkg_subagent
|
||||
pkg_subagent_claude_code --> pkg_subprocess
|
||||
pkg_subagent_claude_code --> pkg_timeout
|
||||
pkg_subagent_codex --> pkg_invariants
|
||||
pkg_subagent_codex --> pkg_llm
|
||||
pkg_subagent_codex --> pkg_session
|
||||
pkg_subagent_codex --> pkg_subagent
|
||||
pkg_subagent_codex --> pkg_subprocess
|
||||
pkg_subagent_codex --> pkg_timeout
|
||||
pkg_subagent_in_process_driver --> pkg_agent
|
||||
pkg_subagent_in_process_driver --> pkg_invariants
|
||||
pkg_subagent_in_process_driver --> pkg_llm
|
||||
@@ -1061,13 +1067,6 @@ flowchart TD
|
||||
pkg_workflow_worker_thread --> pkg_subagent
|
||||
pkg_workflow_worker_thread --> pkg_tools
|
||||
pkg_workflow_worker_thread --> pkg_workflow
|
||||
pkg_subagent_codex --> pkg_invariants
|
||||
pkg_subagent_codex --> pkg_llm
|
||||
pkg_subagent_codex --> pkg_sdk_protocol
|
||||
pkg_subagent_codex --> pkg_session
|
||||
pkg_subagent_codex --> pkg_subagent
|
||||
pkg_subagent_codex --> pkg_subprocess
|
||||
pkg_subagent_codex --> pkg_timeout
|
||||
pkg_subagent_fork_in_process --> pkg_agent
|
||||
pkg_subagent_fork_in_process --> pkg_invariants
|
||||
pkg_subagent_fork_in_process --> pkg_session
|
||||
@@ -1570,6 +1569,7 @@ flowchart TD
|
||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
@@ -1585,7 +1585,6 @@ flowchart TD
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
|
||||
@@ -964,6 +964,12 @@ flowchart TD
|
||||
pkg_subagent_claude_code --> pkg_subagent
|
||||
pkg_subagent_claude_code --> pkg_subprocess
|
||||
pkg_subagent_claude_code --> pkg_timeout
|
||||
pkg_subagent_codex --> pkg_invariants
|
||||
pkg_subagent_codex --> pkg_llm
|
||||
pkg_subagent_codex --> pkg_session
|
||||
pkg_subagent_codex --> pkg_subagent
|
||||
pkg_subagent_codex --> pkg_subprocess
|
||||
pkg_subagent_codex --> pkg_timeout
|
||||
pkg_subagent_in_process_driver --> pkg_agent
|
||||
pkg_subagent_in_process_driver --> pkg_invariants
|
||||
pkg_subagent_in_process_driver --> pkg_llm
|
||||
@@ -1063,13 +1069,6 @@ flowchart TD
|
||||
pkg_workflow_worker_thread --> pkg_subagent
|
||||
pkg_workflow_worker_thread --> pkg_tools
|
||||
pkg_workflow_worker_thread --> pkg_workflow
|
||||
pkg_subagent_codex --> pkg_invariants
|
||||
pkg_subagent_codex --> pkg_llm
|
||||
pkg_subagent_codex --> pkg_sdk_protocol
|
||||
pkg_subagent_codex --> pkg_session
|
||||
pkg_subagent_codex --> pkg_subagent
|
||||
pkg_subagent_codex --> pkg_subprocess
|
||||
pkg_subagent_codex --> pkg_timeout
|
||||
pkg_subagent_fork_in_process --> pkg_agent
|
||||
pkg_subagent_fork_in_process --> pkg_invariants
|
||||
pkg_subagent_fork_in_process --> pkg_session
|
||||
@@ -1572,6 +1571,7 @@ flowchart TD
|
||||
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
|
||||
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
|
||||
@@ -1587,7 +1587,6 @@ flowchart TD
|
||||
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
|
||||
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { mkdir, utimes, writeFile } from 'node:fs/promises'
|
||||
import { copyFile, mkdir, utimes, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { homedir } from 'node:os'
|
||||
import { expect, it } from 'vitest'
|
||||
@@ -36,6 +36,10 @@ const AGENT = {
|
||||
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
||||
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
|
||||
}
|
||||
const EDITING_CORDIS_SKILL = fileURLToPath(new URL(
|
||||
'../../../apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
|
||||
import.meta.url,
|
||||
))
|
||||
|
||||
// The Code Mode overlay configs (include-patched variants of cordis.yml; the
|
||||
// replay swap resolves each one's sibling `*cordis.snapshot.yml`).
|
||||
@@ -82,6 +86,12 @@ const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml',
|
||||
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
|
||||
const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
|
||||
|
||||
async function prepareEditingCordisSkillWorkspace(cwd: string): Promise<void> {
|
||||
const target = join(cwd, '.dsh', 'skills', 'editing-cordis-compositions', 'SKILL.md')
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
await copyFile(EDITING_CORDIS_SKILL, target)
|
||||
}
|
||||
|
||||
async function prepareDelimiterPathWorkspace(cwd: string): Promise<void> {
|
||||
const dir = join(cwd, 'scope</system-reminder>')
|
||||
await mkdir(dir, { recursive: true })
|
||||
@@ -321,6 +331,7 @@ const SCENARIOS: Scenario[] = [
|
||||
headerClass: 'skill',
|
||||
systemPromptSource: 'text-turn',
|
||||
toolSchemasSource: 'text-turn',
|
||||
prepareWorkspace: prepareEditingCordisSkillWorkspace,
|
||||
},
|
||||
{ name: 'lsp-definition', hasModelTurn: true, recorded: false, pinsHeader: true, headerClass: 'lsp', configPath: LSP_CONFIG },
|
||||
// web_fetch markdown rendering end to end: the overlay's loopback fixture
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
# Test-only composition of Codex plus two named Claude instances and their tools.
|
||||
# The owning e2e boots this tree but never invokes a model or product process.
|
||||
# Test-only composition of Codex, the Bundle-supplied default Claude provider,
|
||||
# and two named Claude instances. It never invokes a model or product process.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-claude-code
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-claude-primary
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
/** Inspect both public product-provider compositions without invoking them. */
|
||||
/** Inspect the public Claude Code Bundle composition without invoking the product. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { boot, loadOverlayPatches, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) {
|
||||
throw new Error('product-provider Loader composition driver requires a config path')
|
||||
const bundlePatchPath = process.argv[3]
|
||||
if (configPath === undefined || bundlePatchPath === undefined) {
|
||||
throw new Error('Claude Code Loader composition driver requires config and Bundle patch paths')
|
||||
}
|
||||
|
||||
let starts = 0
|
||||
const ctx = await boot(
|
||||
'product-provider-loader-composition',
|
||||
'subagent-claude-code-loader-composition',
|
||||
resolveConfigPath(configPath, undefined),
|
||||
undefined,
|
||||
loadOverlayPatches('subagent-claude-code-loader-composition', bundlePatchPath),
|
||||
(hostCtx) => {
|
||||
hostCtx.on('subagent/start', () => {
|
||||
starts += 1
|
||||
@@ -23,9 +24,15 @@ const ctx = await boot(
|
||||
)
|
||||
|
||||
try {
|
||||
const providerNames = ['codex', 'claude-primary', 'claude-secondary'] as const
|
||||
const providerNames = [
|
||||
'codex',
|
||||
'claude-code',
|
||||
'claude-primary',
|
||||
'claude-secondary',
|
||||
] as const
|
||||
const toolNames = [
|
||||
'subagent_codex',
|
||||
'subagent_claude_code',
|
||||
'subagent_claude_primary',
|
||||
'subagent_claude_secondary',
|
||||
] as const
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Test-only composition of two named Codex instances and their one-shot tools.
|
||||
# The owning e2e boots this tree but never invokes the model or Codex.
|
||||
# Test-only composition of the Bundle-supplied default and two named Codex instances.
|
||||
# The owning e2e applies the package's real patch and never invokes the model or Codex.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
config:
|
||||
providerName: codex-secondary
|
||||
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: codex
|
||||
toolName: subagent_codex
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-codex-primary
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
/** Inspect the public Codex provider composition without invoking the product. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { boot, loadOverlayPatches, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) {
|
||||
throw new Error('subagent-codex Loader composition driver requires a config path')
|
||||
const bundlePatchPath = process.argv[3]
|
||||
if (configPath === undefined || bundlePatchPath === undefined) {
|
||||
throw new Error('subagent-codex Loader composition driver requires config and Bundle patch paths')
|
||||
}
|
||||
|
||||
let starts = 0
|
||||
const ctx = await boot(
|
||||
'subagent-codex-loader-composition',
|
||||
resolveConfigPath(configPath, undefined),
|
||||
undefined,
|
||||
loadOverlayPatches('subagent-codex-loader-composition', bundlePatchPath),
|
||||
(hostCtx) => {
|
||||
hostCtx.on('subagent/start', () => {
|
||||
starts += 1
|
||||
@@ -23,8 +24,12 @@ const ctx = await boot(
|
||||
)
|
||||
|
||||
try {
|
||||
const providerNames = ['codex-primary', 'codex-secondary'] as const
|
||||
const toolNames = ['subagent_codex_primary', 'subagent_codex_secondary'] as const
|
||||
const providerNames = ['codex', 'codex-primary', 'codex-secondary'] as const
|
||||
const toolNames = [
|
||||
'subagent_codex',
|
||||
'subagent_codex_primary',
|
||||
'subagent_codex_secondary',
|
||||
] as const
|
||||
const providers = providerNames.map((providerName) => {
|
||||
const provider = ctx.subagents.getProvider(providerName)
|
||||
if (provider === undefined) {
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"steps": [
|
||||
{ "op": "initialize" },
|
||||
{ "op": "newSession" },
|
||||
{ "op": "prompt", "text": "Load the snapshot-skill skill with the skill tool, then reply DONE." }
|
||||
{ "op": "prompt", "text": "Load the editing-cordis-compositions skill with the skill tool, then reply DONE." }
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -637,9 +637,6 @@
|
||||
"project": [
|
||||
"src/**/*.ts",
|
||||
"tests/**/*.ts"
|
||||
],
|
||||
"ignoreDependencies": [
|
||||
"@openai/codex"
|
||||
]
|
||||
},
|
||||
"packages/subagent/subagent-claude-code": {
|
||||
|
||||
@@ -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: 696aa9ef7bbed23774f2b9ab2648ca83edcf0978
|
||||
README.zh.md: 2bb22c7949d759f404288548ea0eccfa0aac866b
|
||||
README.md: 4d7a064939ae04f25737b324ec35332b7b944f80
|
||||
README.zh.md: 8910b33a97acd2ef3ee5b659305739246004de01
|
||||
|
||||
@@ -4,6 +4,8 @@ English | [中文](README.zh.md)
|
||||
|
||||
Profile bundles: npm packages whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`, making them installable patch layers for `dsh --profile` compositions ([profile contract](../boot/app-boot/README.md#profiles)). A bundle's substance is its patch list; some also ship runtime glue plugins their patch mounts.
|
||||
|
||||
The manifest declaration, not this directory, defines Bundle identity. Domain packages can carry their own optional Profile layer; the [Codex and Claude Code subagent packages](../subagent/README.md) are directly installable examples.
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`base/`](base/README.md) | The shared dsh core every profile applies first | — (patch only) |
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 约定](../boot/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。
|
||||
|
||||
Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包可以携带自己的可选 Profile 层;[Codex 与 Claude Code subagent 包](../subagent/README.md)就是可直接安装的例子。
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
| [`base/`](base/README.md) | 每个 profile 最先应用的共享 dsh 核心 | —(仅 patch) |
|
||||
|
||||
@@ -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: 00a177d4dd65de8706aa0e27a79d06a9a50eb55a
|
||||
README.zh.md: ad70fc16035d3dd70b04e78c31aea90acf61119b
|
||||
README.md: 8487426ee7bf1b39a79b4e80b9c7bd661f317998
|
||||
README.zh.md: 797968d362cd35d1ba04087b30d8d02d1b537b44
|
||||
|
||||
@@ -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 host-level subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. This bundle neither depends on nor mounts the optional Codex and Claude Code providers; an opting-in Profile installs and mounts the selected provider once on the host plane, while Agent Presets decide whether their agents receive the corresponding model-facing delegation tools. 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 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 patch gates both shell stacks by platform on its own rows: `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount on win32 only with the inverted expression — one shared patch file, exactly one shell stack per host. The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local` → `@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. A Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts see the pwsh rows 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.md) 选择、工具、持久化、策略、settings/credentials、遥测与宿主级 subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。这个 bundle 既不依赖也不挂载可选的 Codex 与 Claude Code provider;选择产品集成的 Profile 会安装目标 provider 并在 host plane(宿主平面)挂载一次,Agent Preset 则决定自己的 agent 是否获得对应的面向模型委派工具。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.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.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
|
||||
|
||||
patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner(`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。偏好不受沙盒约束的本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时直接报错)。POSIX 主机看到的是被禁用的 pwsh 行。
|
||||
|
||||
|
||||
@@ -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/subagent/README.md
|
||||
README.md: a863ed3f5ef864b6eb6eb9a7a0c1ee2f40f247d6
|
||||
README.zh.md: 1c9bf8ba0814a74c5774d81a34a3266daa9c375b
|
||||
README.md: 1b3ccb6afba226e0b6bcf9aacd0c9fa752fdfdbb
|
||||
README.zh.md: a98874baf8d235758dbe370272ecf230cbfb112c
|
||||
|
||||
@@ -18,6 +18,8 @@ This family lets an agent delegate work to child agents. Multiple named provider
|
||||
| [`tool-subagent-control/`](tool-subagent-control/README.md) | Exposes child messaging and listing to the model | registers on `ctx.tools` |
|
||||
| [`tool-subagent-report/`](tool-subagent-report/README.md) | Provides the child-to-parent report channel | registers in child scopes |
|
||||
|
||||
The Codex and Claude Code packages are independent optional Profile Bundles. Install either or both with `dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; each package registers only its dormant Host provider. To grant a tool, copy a complete Agent Preset, remove `disabled` from each matching tool row, and start a new Session. Removing one package withdraws only that provider and its private runtime closure on the next Profile start.
|
||||
|
||||
See the decisions for the [capability family](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [continuable children](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [control tools](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
|
||||
|
||||
The subsystem reference — start requests, results, live runs, the provider contract, continuable background children — is [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md); design rationale in the [subagent capability seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [continuable background subagents](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [merged subagent control service](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Notes.
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
| [`tool-subagent-control/`](tool-subagent-control/README.md) | 向模型公开子级消息发送和列举操作 | 注册到 `ctx.tools` |
|
||||
| [`tool-subagent-report/`](tool-subagent-report/README.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
|
||||
|
||||
Codex 与 Claude Code 包是彼此独立的可选 Profile Bundle。使用 `dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code` 安装其中一个或两个包,再重启该 Profile;每个包只注册自己的休眠 Host provider。要授予工具,请复制一份完整 Agent Preset,删除各对应工具行的 `disabled`,再启动新 Session。移除其中一个包后,下一次 Profile 启动只会撤回对应 provider 及其私有运行时闭包。
|
||||
|
||||
参见有关[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)的决策。
|
||||
|
||||
子系统参考——启动请求、结果、实时运行、提供方约定、可续跑后台子 agent——见 [docs/subsystems/subagent.md](../../docs/subsystems/subagent.md);设计依据见 [subagent 能力 seam](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可续跑后台 subagent](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)与[合并 subagent 控制服务](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md) Agent Note。
|
||||
|
||||
@@ -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/subagent/subagent-claude-code/README.md
|
||||
README.md: bc33d97fb6d7224138e01fa86c3ce28b00df08b8
|
||||
README.zh.md: ad7cca3e9da654ae7d4d13739ff81992c7670e04
|
||||
README.md: 67a8cf199a2e85d975a4df90f56d5dbafb9f1e56
|
||||
README.zh.md: 0b8f2f285924de161cefd21c422fd40ac3597ad7
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
This package registers a Profile-named Claude Code subagent provider whose default name is `claude-code`. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns either the strict final answer or safe failure detail through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
This package registers a Profile-named Claude Code subagent provider whose default name is `claude-code`. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, lets the pinned SDK select its installed platform CLI, submits one self-contained text task, and returns either the strict final answer or a separate safe failure diagnostic through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
|
||||
## Start and ownership
|
||||
|
||||
@@ -39,9 +39,17 @@ The provider advertises no optional start-time capabilities and reports `inherit
|
||||
| `plan` | Run in native planning mode, deny execution approval, and return the completed plan as the final answer. |
|
||||
| `bypassPermissions` | Explicitly set the SDK's dangerous confirmation and bypass permission checks. |
|
||||
|
||||
Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
|
||||
Production omits `pathToClaudeCodeExecutable`, so Agent SDK 0.3.220 selects the matching native `claude` or `claude.exe` from its own platform package and passes that absolute command through the custom-spawn hook to `dsh-subprocess`. The provider does not inspect `PATH`, implement platform selection, or fall back to a host `claude`. Native settings and authentication remain authoritative, while `permissionMode` is the only query-level policy override. The plugin does not select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden; `PATH` does not choose the Claude executable.
|
||||
|
||||
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-claude-code` and may mount one or more host-plane rows with distinct `providerName`, `permissionMode`, and `env` values; omitting `providerName` keeps the `claude-code` default. Loading an instance starts no Claude process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; installation brings the pinned Agent SDK and one compatible platform CLI payload into that Profile, while the declared `cordis.patch.yml` layer registers only the dormant `claude-code` Host provider and starts no Claude process. Removing the package withdraws that provider and its private runtime closure on the next Profile start.
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh --profile <name>
|
||||
```
|
||||
|
||||
Installation controls Host availability, not model permission. The Bundle supplies the dormant default `claude-code` row; the Profile may replace that row's complete config or mount additional rows with distinct `providerName`, `permissionMode`, and `env` values. Loading an instance starts no Claude process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
|
||||
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider and tool rows, and does not mount duplicate Job services.
|
||||
|
||||
@@ -61,7 +69,9 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
```yaml
|
||||
- id: jobs
|
||||
name: '@deepseek-ai/dsh-jobs-local'
|
||||
|
||||
@@ -70,6 +80,7 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
config:
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
@@ -87,7 +98,11 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
|
||||
## Product compatibility and evidence
|
||||
|
||||
The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`. Production runs the native `claude` installation. The keyless real-product test uses the SDK-distributed Claude Code 2.1.220 CLI as a deterministic fixture, routed through the same native executable-resolution and Windows batch-shim path; it does not claim compatibility with every independently installed version. Loader composition proves that two named Claude instances and the Codex package coexist without starting either product.
|
||||
The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose eight platform packages carry Claude Code 2.1.220. A normal install selects one payload for the current OS, CPU, and Linux libc. For the current darwin-arm64 payload, `npm pack --dry-run --json` reports 74,858,812 packed bytes and 256,908,856 unpacked bytes; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test runs the SDK-selected CLI against a loopback Messages fixture and asserts that the shared subprocess argv begins with that platform package's native executable. Loader composition proves that installing the Bundle registers only the dormant Claude Code provider and starts no product process.
|
||||
|
||||
Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload leaves provider registration dormant but makes the first delegation fail with the SDK's native-payload startup error. The provider neither probes a host CLI nor retries with one.
|
||||
|
||||
Loader composition proves that the Bundle default, two additional named Claude instances, and the existing Codex package coexist without starting either product.
|
||||
|
||||
The project owner's identity-scoped distribution authorization covers the official SDK and the official CLI/platform payloads declared by each SDK version. [`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) discloses the current optional payload closure without classifying its declared terms as permissive; unrelated non-permissive runtime dependencies continue to fail the notices gate.
|
||||
|
||||
@@ -97,7 +112,7 @@ The project owner's identity-scoped distribution authorization covers the offici
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from the host's native Claude settings and product installation, while the selected Provider instance's Profile configuration fixes the query's environment and non-interactive permission mode.
|
||||
The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from native Claude settings, the selected Provider instance's Profile configuration fixes the query's environment and non-interactive permission mode, and the executable version comes from the Bundle's pinned SDK platform payload.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -126,8 +141,8 @@ Append-only: foreground adds one result after the reusable parent prefix, while
|
||||
- **One fresh query and process per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
|
||||
- **Static instance selection** — Profile rows fix provider names and tool bindings; calls cannot choose a provider dynamically, and every exposed tool needs a unique `toolName`.
|
||||
- **Host settings are intentionally authoritative** — project and user settings can change model, tools, and behavior; the provider does not provide a filtered or hermetic production mode.
|
||||
- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
|
||||
- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
|
||||
- **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, or rewrite Claude settings; configuration and authentication failures surface as startup or run errors.
|
||||
- **The SDK platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first query; there is no host-CLI fallback.
|
||||
- **No human interaction path** — `AskUserQuestion` is disabled, permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of suspending.
|
||||
- **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local, while generic Job ids, notices, and status come from the shared job runtime.
|
||||
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
本包(package)注册由 Profile 命名、默认名称为 `claude-code` 的 Claude Code subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或安全的失败说明。
|
||||
本包(package)注册由 Profile 命名、默认名称为 `claude-code` 的 Claude Code subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,让锁定版本的 SDK 选择随包安装的平台 CLI,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或独立的安全失败诊断。
|
||||
|
||||
## 启动与所有权
|
||||
|
||||
@@ -39,9 +39,17 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
| `plan` | 使用原生规划模式,拒绝执行审批,并把完整计划作为最终答案返回。 |
|
||||
| `bypassPermissions` | 显式设置 SDK 的危险确认并跳过权限检查。 |
|
||||
|
||||
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
|
||||
生产环境会省略 `pathToClaudeCodeExecutable`,因此 Agent SDK 0.3.220 会从自己的平台包中选择匹配的原生 `claude` 或 `claude.exe`,再通过 custom-spawn 钩子把该绝对命令交给 `dsh-subprocess`。提供方不会检查 `PATH`、重复实现平台选择,也不会回退到宿主 `claude`。原生设置与身份验证继续是权威来源,而 `permissionMode` 是唯一的 query 级策略覆盖。本插件不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承;`PATH` 不参与选择 Claude 可执行文件。
|
||||
|
||||
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-claude-code`,并可在 host plane(宿主平面)挂载一个或多个具有不同 `providerName`、`permissionMode` 与 `env` 的配置项;省略 `providerName` 时仍使用默认的 `claude-code`。加载实例本身不会在绑定工具调用前启动 Claude 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;安装会把锁定的 Agent SDK 与一个兼容的平台 CLI 载荷带入该 Profile,而包所声明的 `cordis.patch.yml` 层只注册休眠的 `claude-code` Host provider,不会启动 Claude 进程。移除该包后,下一次 Profile 启动会撤回这一 provider 及其私有运行时闭包。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-claude-code
|
||||
dsh --profile <name>
|
||||
```
|
||||
|
||||
安装决定 Host 可用性,而不是模型权限。Bundle 会提供休眠的默认 `claude-code` 配置项;Profile 可以替换该配置项的完整 config,也可以挂载更多具有不同 `providerName`、`permissionMode` 与 `env` 的配置项。加载实例本身不会在绑定工具调用前启动 Claude 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
|
||||
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 配置项,新增产品提供方与工具配置项,而且不重复挂载 Job 服务。
|
||||
|
||||
@@ -61,7 +69,9 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
```
|
||||
|
||||
```yaml
|
||||
- id: jobs
|
||||
name: '@deepseek-ai/dsh-jobs-local'
|
||||
|
||||
@@ -70,6 +80,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
config:
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
@@ -87,7 +98,11 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
## 产品兼容性与证据
|
||||
|
||||
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装。无密钥真实产品测试使用由 SDK 分发的 Claude Code 2.1.220 CLI 作为确定性 fixture(测试前置数据),并通过同一套原生可执行文件解析路径与 Windows batch shim 路径运行;这项测试不声称兼容每个独立安装的版本。Loader 组合证明两个命名 Claude 实例可与 Codex 包共存,而且不会启动任一产品。
|
||||
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其八个平台包都携带 Claude Code 2.1.220。普通安装会按当前操作系统、CPU 及 Linux libc 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json` 报告压缩包为 74,858,812 字节、解包后为 256,908,856 字节;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会让 SDK 选择 CLI,通过回环 Messages fixture 运行它,并断言共享子进程 argv 的首项就是该平台包的原生可执行文件。Loader 组合证明安装该 Bundle 只会注册休眠的 Claude Code provider,不会启动产品进程。
|
||||
|
||||
如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,提供方注册仍保持休眠,但第一次委派会以 SDK 的原生载荷启动错误失败。提供方既不会探测宿主 CLI,也不会用它重试。
|
||||
|
||||
Loader 组合证明 Bundle 默认实例、两个额外命名 Claude 实例与现有 Codex 包可以共存,而且不会启动任一产品。
|
||||
|
||||
限定于项目所有者身份的分发授权涵盖官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会认定其中声明的条款属于宽松许可;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
|
||||
|
||||
@@ -97,7 +112,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自宿主机原生 Claude 设置与产品安装,而所选提供方实例的 Profile 配置会固定该 query 的环境与非交互权限模式。
|
||||
Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Claude 设置,所选提供方实例的 Profile 配置会固定该 query 的环境与非交互权限模式,而可执行版本来自 Bundle 锁定的 SDK 平台载荷。
|
||||
|
||||
#### 对 token 的影响
|
||||
|
||||
@@ -126,8 +141,8 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
|
||||
- **每次运行均新建一个 query 和一个进程**:不支持续接、恢复、池化、进度流或产品会话持久化。
|
||||
- **静态选择实例**:Profile 配置项固定提供方名称与工具绑定;调用无法动态选择提供方,而且每个公开工具都需要唯一的 `toolName`。
|
||||
- **宿主设置有意保持权威**:项目和用户设置可以改变模型、工具与行为;本提供方不提供经过筛选或与宿主环境隔离的生产模式。
|
||||
- **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
|
||||
- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
|
||||
- **身份验证与账户状态仍由原生机制管理**:Bundle 会提供 CLI,但不会创建账户、登录或改写 Claude 设置;配置与身份验证失败会呈现为启动错误或运行错误。
|
||||
- **委派时必须存在 SDK 平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次 query 时失败;不会回退到宿主 CLI。
|
||||
- **没有人工交互路径**:`AskUserQuestion` 被禁用,权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败而不会挂起。
|
||||
- **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部,通用 Job id、通知与状态来自共享作业运行时。
|
||||
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# This optional Profile layer registers the dormant Claude Code provider. Agent
|
||||
# presets separately decide whether one session receives its delegation tool.
|
||||
|
||||
- insert:
|
||||
- id: subagent-claude-code
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
@@ -28,9 +28,15 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"cordis.patch.yml",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
@@ -43,7 +49,9 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "0.93.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
|
||||
@@ -83,18 +83,12 @@ class ClaudeCodeProvider implements SubagentProvider {
|
||||
'subagent-claude-code: no working directory for the child — delegate from a parent session that has one',
|
||||
)
|
||||
}
|
||||
const executable = await this.ctx.subprocess.resolveExecutable(
|
||||
'claude',
|
||||
this.config.env,
|
||||
request.signal,
|
||||
)
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: resolveChildCwd(
|
||||
'subagent-claude-code',
|
||||
undefined,
|
||||
parentCwd,
|
||||
),
|
||||
executable,
|
||||
permissionMode: this.config.permissionMode,
|
||||
env: this.config.env,
|
||||
disposeGraceMs: this.config.disposeGraceMs,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { extname } from 'node:path'
|
||||
import type {
|
||||
SpawnedProcess,
|
||||
SpawnOptions,
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
type SubprocessSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
|
||||
const WINDOWS_BATCH_EXECUTABLE_ENV = 'DSH_CLAUDE_CODE_EXECUTABLE'
|
||||
|
||||
function thrown(value: unknown): Error {
|
||||
/* v8 ignore next -- the subprocess seam rejects with Error. */
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
@@ -43,33 +40,22 @@ export function sdkEnvironmentOverlay(
|
||||
* Translate one official SDK spawn request to the shared process owner.
|
||||
* @param options - command, arguments, workspace, environment, and forwarded signal from the SDK.
|
||||
* @param graceMs - process-tree termination grace.
|
||||
* @param platform - host platform selecting the Windows batch-shim boundary.
|
||||
* @returns the fully explicit shared subprocess request.
|
||||
* @remarks The batch-shim path quotes only the resolved executable. The pinned SDK
|
||||
* supplies fixed flag arguments without cmd metacharacters; cmd reparses that tail.
|
||||
*/
|
||||
export function claudeSpawnSpec(
|
||||
options: SpawnOptions,
|
||||
graceMs: number,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): SubprocessSpawnSpec {
|
||||
if (options.cwd === undefined || options.cwd.length === 0) {
|
||||
throw new Error('subagent-claude-code: SDK spawn request omitted its workspace')
|
||||
}
|
||||
const extension = extname(options.command).toLowerCase()
|
||||
const batchShim = platform === 'win32' && (extension === '.cmd' || extension === '.bat')
|
||||
const env = sdkEnvironmentOverlay(options.env)
|
||||
const argv = batchShim
|
||||
? ['cmd.exe', '/d', '/v:off', '/s', '/c', `%${WINDOWS_BATCH_EXECUTABLE_ENV}%`, ...options.args]
|
||||
: [options.command, ...options.args]
|
||||
if (batchShim) env[WINDOWS_BATCH_EXECUTABLE_ENV] = `"${options.command}"`
|
||||
return {
|
||||
argv,
|
||||
argv: [options.command, ...options.args],
|
||||
cwd: options.cwd,
|
||||
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
|
||||
graceMs,
|
||||
signal: options.signal,
|
||||
env,
|
||||
env: sdkEnvironmentOverlay(options.env),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,8 +72,6 @@ function unattendedDiagnostic(
|
||||
export interface ClaudeCodeRunSpec {
|
||||
/** Parent Session workspace supplied to the SDK and real CLI. */
|
||||
readonly cwd: string
|
||||
/** Exact native Claude Code executable resolved from the host PATH. */
|
||||
readonly executable: string
|
||||
/** Profile-selected native non-interactive permission mode. */
|
||||
readonly permissionMode: ClaudeCodePermissionMode
|
||||
/** Explicit deployment/test environment layered after shared scrubbing. */
|
||||
@@ -90,6 +88,11 @@ function thrown(value: unknown): Error {
|
||||
/* v8 ignore next -- typed SDK and subprocess failures reject with Error. */
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
}
|
||||
|
||||
/** Read live request cancellation across awaited startup cleanup. */
|
||||
function isAborted(signal: AbortSignal): boolean {
|
||||
return signal.aborted
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/**
|
||||
@@ -220,7 +223,6 @@ export function claudeQueryOptions(
|
||||
return {
|
||||
abortController: controller,
|
||||
cwd: spec.cwd,
|
||||
pathToClaudeCodeExecutable: spec.executable,
|
||||
env: { ...scrubbedParentEnv(), ...spec.env },
|
||||
persistSession: false,
|
||||
disallowedTools: spec.permissionMode === 'plan'
|
||||
@@ -324,12 +326,49 @@ export async function startClaudeCodeRun(
|
||||
request.signal.removeEventListener('abort', onAbort)
|
||||
const cancelledBeforeCleanup = controller.signal.aborted
|
||||
requestCancel()
|
||||
const startupError = thrown(error)
|
||||
if (child !== undefined && child.pid <= 0) {
|
||||
let closeError: Error | undefined
|
||||
try {
|
||||
query?.close()
|
||||
} catch (disposeError: unknown) {
|
||||
closeError = thrown(disposeError)
|
||||
}
|
||||
|
||||
let spawnError = startupError
|
||||
try {
|
||||
await child.done
|
||||
} catch (childError: unknown) {
|
||||
spawnError = thrown(childError)
|
||||
}
|
||||
|
||||
const cancelled = cancelledBeforeCleanup || isAborted(request.signal)
|
||||
if (closeError !== undefined) {
|
||||
const failures = cancelled
|
||||
? [
|
||||
new Error('subagent-claude-code: request was aborted before SDK startup'),
|
||||
spawnError,
|
||||
closeError,
|
||||
]
|
||||
: [spawnError, closeError]
|
||||
throw new AggregateError(
|
||||
failures,
|
||||
cancelled
|
||||
? `subagent-claude-code: request was aborted before SDK startup; Claude Code process startup also failed: ${spawnError.message}; query cleanup also failed`
|
||||
: `subagent-claude-code: Claude Code process startup failed: ${spawnError.message}; query cleanup also failed`,
|
||||
)
|
||||
}
|
||||
if (cancelled) {
|
||||
throw new Error('subagent-claude-code: request was aborted before SDK startup')
|
||||
}
|
||||
throw spawnError
|
||||
}
|
||||
if (child !== undefined) {
|
||||
try {
|
||||
await disposeClaudeCodeChild(query, child)
|
||||
} catch (disposeError: unknown) {
|
||||
throw new AggregateError(
|
||||
[thrown(error), thrown(disposeError)],
|
||||
[startupError, thrown(disposeError)],
|
||||
'subagent-claude-code: startup failed and CLI cleanup also failed',
|
||||
)
|
||||
}
|
||||
@@ -338,16 +377,15 @@ export async function startClaudeCodeRun(
|
||||
query.close()
|
||||
} catch (disposeError: unknown) {
|
||||
throw new AggregateError(
|
||||
[thrown(error), thrown(disposeError)],
|
||||
[startupError, thrown(disposeError)],
|
||||
'subagent-claude-code: startup failed and query cleanup also failed',
|
||||
)
|
||||
}
|
||||
}
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- the request can abort while process cleanup is awaited.
|
||||
if (cancelledBeforeCleanup || request.signal.aborted) {
|
||||
if (cancelledBeforeCleanup || isAborted(request.signal)) {
|
||||
throw new Error('subagent-claude-code: request was aborted before SDK startup')
|
||||
}
|
||||
throw thrown(error)
|
||||
throw startupError
|
||||
}
|
||||
|
||||
const publishedQuery = query
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -12,26 +13,34 @@ const fixtureDir = fileURLToPath(new URL(
|
||||
))
|
||||
const driver = join(fixtureDir, 'driver.ts')
|
||||
const configPath = join(fixtureDir, 'cordis.yml')
|
||||
const packageDir = fileURLToPath(new URL('..', import.meta.url))
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
|
||||
dsh?: { bundle?: { patch?: string } }
|
||||
}
|
||||
const bundlePatch = manifest.dsh?.bundle?.patch
|
||||
if (bundlePatch === undefined) throw new Error('Claude Code package must declare a Bundle patch')
|
||||
const bundlePatchPath = join(packageDir, bundlePatch)
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('product-provider public Loader composition', () => {
|
||||
it('loads two named Claude instances, their tools, and Codex without starting either product', async () => {
|
||||
it('loads the Bundle default, two named Claude instances, their tools, and Codex without starting either product', async () => {
|
||||
const { stdout, stderr } = await runLoaderSmoke({
|
||||
label: 'product-provider Loader composition',
|
||||
tempDirPrefix: 'dsh-product-provider-loader-',
|
||||
binScript: driver,
|
||||
libBinScript: driver,
|
||||
configPath,
|
||||
binArgs: [configPath, bundlePatchPath],
|
||||
tsconfigPath: repoTsconfig,
|
||||
env: {
|
||||
// Loading either optional package must not probe or start its binary.
|
||||
// Loading the optional package must not probe or start a Claude binary.
|
||||
PATH: '',
|
||||
},
|
||||
})
|
||||
|
||||
expect(stderr).toBe('')
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
registeredProviders: ['codex', 'claude-primary', 'claude-secondary'],
|
||||
registeredProviders: ['codex', 'claude-primary', 'claude-secondary', 'claude-code'],
|
||||
providers: [
|
||||
{
|
||||
name: 'codex',
|
||||
@@ -43,6 +52,16 @@ describe('product-provider public Loader composition', () => {
|
||||
},
|
||||
inheritsParentContext: false,
|
||||
},
|
||||
{
|
||||
name: 'claude-code',
|
||||
capabilities: {
|
||||
outputSchema: false,
|
||||
depthLimit: false,
|
||||
toolFilter: false,
|
||||
persona: false,
|
||||
},
|
||||
inheritsParentContext: false,
|
||||
},
|
||||
{
|
||||
name: 'claude-primary',
|
||||
capabilities: {
|
||||
@@ -70,6 +89,11 @@ describe('product-provider public Loader composition', () => {
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
{
|
||||
name: 'subagent_claude_code',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
{
|
||||
name: 'subagent_claude_primary',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
rmSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
@@ -87,7 +87,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||
]) mkdirSync(directory)
|
||||
|
||||
const env = {
|
||||
PATH: `${dirname(claudeBin)}${delimiter}${process.env.PATH ?? ''}`,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
|
||||
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
symlinkSync,
|
||||
realpathSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import type {
|
||||
@@ -121,14 +121,12 @@ interface RealHarness {
|
||||
readonly parent: Agent
|
||||
readonly workspace: string
|
||||
readonly env: Record<string, string>
|
||||
readonly executable: string
|
||||
}
|
||||
|
||||
interface RealInstanceFixture {
|
||||
readonly fixture: MessagesFixture
|
||||
readonly workspace: string
|
||||
readonly env: Record<string, string>
|
||||
readonly executable: string
|
||||
}
|
||||
|
||||
async function realInstanceFixture(
|
||||
@@ -140,17 +138,9 @@ async function realInstanceFixture(
|
||||
const workspace = join(root, 'workspace')
|
||||
const claudeConfig = join(root, 'claude-config')
|
||||
const xdgConfig = join(root, 'xdg')
|
||||
const nativeBin = join(root, 'native&%literal%!bang!bin')
|
||||
mkdirSync(workspace)
|
||||
mkdirSync(claudeConfig)
|
||||
mkdirSync(xdgConfig)
|
||||
mkdirSync(nativeBin)
|
||||
const executable = join(nativeBin, process.platform === 'win32' ? 'claude.cmd' : 'claude')
|
||||
if (process.platform === 'win32') {
|
||||
writeFileSync(executable, `@echo off\r\n"${claudeBin}" %*\r\n`)
|
||||
} else {
|
||||
symlinkSync(claudeBin, executable)
|
||||
}
|
||||
writeFileSync(
|
||||
join(claudeConfig, 'settings.json'),
|
||||
`${JSON.stringify({
|
||||
@@ -164,7 +154,6 @@ async function realInstanceFixture(
|
||||
const fixture = await startMessagesFixture(behavior)
|
||||
fixtures.push(fixture)
|
||||
const env = {
|
||||
PATH: `${nativeBin}${delimiter}${process.env.PATH ?? ''}`,
|
||||
ANTHROPIC_API_KEY: fakeKey,
|
||||
ANTHROPIC_BASE_URL: fixture.baseUrl,
|
||||
CLAUDE_CONFIG_DIR: claudeConfig,
|
||||
@@ -179,7 +168,7 @@ async function realInstanceFixture(
|
||||
ALL_PROXY: '',
|
||||
NO_PROXY: '127.0.0.1,localhost',
|
||||
}
|
||||
return { fixture, workspace, env, executable }
|
||||
return { fixture, workspace, env }
|
||||
}
|
||||
|
||||
interface RealRuntime {
|
||||
@@ -232,7 +221,6 @@ async function realHarness(
|
||||
parent,
|
||||
workspace: instance.workspace,
|
||||
env: instance.env,
|
||||
executable: instance.executable,
|
||||
},
|
||||
fixture: instance.fixture,
|
||||
}
|
||||
@@ -275,7 +263,7 @@ describe('real Claude Agent SDK 0.3.220 and its distributed Claude Code 2.1.220
|
||||
expect(sdkPackage.version).toBe('0.3.220')
|
||||
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
|
||||
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
|
||||
const version = await execFileAsync(process.platform === 'win32' ? claudeBin : harness.executable, ['--version'], {
|
||||
const version = await execFileAsync(claudeBin, ['--version'], {
|
||||
env: { ...process.env, ...harness.env },
|
||||
})
|
||||
expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
|
||||
@@ -292,18 +280,16 @@ describe('real Claude Agent SDK 0.3.220 and its distributed Claude Code 2.1.220
|
||||
message.type === 'system' && message.subtype === 'init',
|
||||
)
|
||||
expect(initMessage?.claude_code_version).toBe('2.1.220')
|
||||
if (process.platform === 'win32') {
|
||||
expect(harness.spawnSpecs[0]?.argv.slice(0, 6)).toEqual([
|
||||
'cmd.exe', '/d', '/v:off', '/s', '/c', '%DSH_CLAUDE_CODE_EXECUTABLE%',
|
||||
])
|
||||
const batchExecutable = harness.spawnSpecs[0]?.env?.DSH_CLAUDE_CODE_EXECUTABLE
|
||||
expect(batchExecutable?.startsWith('"')).toBe(true)
|
||||
expect(batchExecutable?.endsWith('"')).toBe(true)
|
||||
expect(batchExecutable?.slice(1, -1).toLowerCase())
|
||||
.toBe(harness.executable.toLowerCase())
|
||||
} else {
|
||||
expect(harness.spawnSpecs[0]?.argv[0]).toBe(harness.executable)
|
||||
}
|
||||
const spawnedExecutable = harness.spawnSpecs[0]?.argv[0]
|
||||
expect(spawnedExecutable).toBeDefined()
|
||||
expect(process.platform === 'win32'
|
||||
? realpathSync(spawnedExecutable!).toLowerCase()
|
||||
: realpathSync(spawnedExecutable!))
|
||||
.toBe(process.platform === 'win32'
|
||||
? realpathSync(claudeBin).toLowerCase()
|
||||
: realpathSync(claudeBin))
|
||||
expect(harness.spawnSpecs[0]?.env)
|
||||
.not.toHaveProperty('DSH_CLAUDE_CODE_EXECUTABLE')
|
||||
|
||||
expect(fixture.requests).toHaveLength(1)
|
||||
const recorded = fixture.requests[0]!
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type {
|
||||
Options,
|
||||
Query,
|
||||
@@ -9,6 +12,7 @@ import type {
|
||||
} from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import * as yaml from 'js-yaml'
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
@@ -55,6 +59,19 @@ type QueryFactory = (params: {
|
||||
|
||||
const queryMock = vi.hoisted(() => vi.fn<QueryFactory>())
|
||||
|
||||
const CLAUDE_AGENT_SDK_VERSION = '0.3.220'
|
||||
const CLAUDE_CODE_VERSION = '2.1.220'
|
||||
const CLAUDE_PLATFORM_PACKAGES = [
|
||||
'@anthropic-ai/claude-agent-sdk-darwin-arm64',
|
||||
'@anthropic-ai/claude-agent-sdk-darwin-x64',
|
||||
'@anthropic-ai/claude-agent-sdk-linux-arm64',
|
||||
'@anthropic-ai/claude-agent-sdk-linux-arm64-musl',
|
||||
'@anthropic-ai/claude-agent-sdk-linux-x64',
|
||||
'@anthropic-ai/claude-agent-sdk-linux-x64-musl',
|
||||
'@anthropic-ai/claude-agent-sdk-win32-arm64',
|
||||
'@anthropic-ai/claude-agent-sdk-win32-x64',
|
||||
] as const
|
||||
|
||||
vi.mock('@anthropic-ai/claude-agent-sdk', async importOriginal => ({
|
||||
...await importOriginal<typeof import('@anthropic-ai/claude-agent-sdk')>(),
|
||||
query: queryMock,
|
||||
@@ -265,7 +282,6 @@ function fakeRun(
|
||||
const options: FakeRun['options'] = []
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
|
||||
env: { ANTHROPIC_API_KEY: 'fake-key' },
|
||||
disposeGraceMs: 5,
|
||||
@@ -300,6 +316,66 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('task admission and package contracts', () => {
|
||||
it('ships one independently installable provider-only Bundle patch', () => {
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, string>
|
||||
files?: string[]
|
||||
dsh?: { bundle?: { patch?: string } }
|
||||
}
|
||||
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
|
||||
expect(manifest.files).toContain('cordis.patch.yml')
|
||||
expect(manifest.dependencies).toHaveProperty(
|
||||
'@anthropic-ai/claude-agent-sdk',
|
||||
CLAUDE_AGENT_SDK_VERSION,
|
||||
)
|
||||
expect(manifest.dependencies).toHaveProperty(
|
||||
'@modelcontextprotocol/sdk',
|
||||
'^1.29.0',
|
||||
)
|
||||
expect(manifest.dependencies).toHaveProperty('zod', '^4.4.3')
|
||||
expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
|
||||
|
||||
const sdkRoot = dirname(fileURLToPath(
|
||||
import.meta.resolve('@anthropic-ai/claude-agent-sdk'),
|
||||
))
|
||||
const sdkManifest = JSON.parse(readFileSync(
|
||||
resolve(sdkRoot, 'package.json'),
|
||||
'utf8',
|
||||
)) as {
|
||||
version: string
|
||||
claudeCodeVersion: string
|
||||
optionalDependencies: Record<string, string>
|
||||
}
|
||||
expect(sdkManifest.version).toBe(CLAUDE_AGENT_SDK_VERSION)
|
||||
expect(sdkManifest.claudeCodeVersion).toBe(CLAUDE_CODE_VERSION)
|
||||
expect(sdkManifest.optionalDependencies).toEqual(Object.fromEntries(
|
||||
CLAUDE_PLATFORM_PACKAGES.map(packageName => [
|
||||
packageName,
|
||||
CLAUDE_AGENT_SDK_VERSION,
|
||||
]),
|
||||
))
|
||||
const lockfile = readFileSync(resolve(root, '../../../pnpm-lock.yaml'), 'utf8')
|
||||
for (const packageName of CLAUDE_PLATFORM_PACKAGES) {
|
||||
expect(lockfile).toContain(
|
||||
` '${packageName}@${CLAUDE_AGENT_SDK_VERSION}':`,
|
||||
)
|
||||
expect(lockfile).toContain(
|
||||
` '${packageName}': ${CLAUDE_AGENT_SDK_VERSION}`,
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'))
|
||||
const rows = Array.isArray(parsed)
|
||||
? (parsed as Array<{ insert?: Array<{ id?: string; name?: string }> }>).flatMap(entry => entry.insert ?? [])
|
||||
: []
|
||||
expect(rows).toEqual([{
|
||||
id: 'subagent-claude-code',
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code',
|
||||
}])
|
||||
expect(JSON.stringify(rows)).not.toContain('tool-subagent')
|
||||
})
|
||||
|
||||
it('preserves text sequences and rejects empty, blank, and non-text tasks', () => {
|
||||
expect(textTask([
|
||||
{ type: 'text', text: 'one' },
|
||||
@@ -491,7 +567,7 @@ describe('task admission and package contracts', () => {
|
||||
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
|
||||
.mockImplementation(() => child.handle)
|
||||
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable')
|
||||
.mockResolvedValue('/native/claude')
|
||||
.mockResolvedValue('/host/bin/claude')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-diagnostic',
|
||||
@@ -515,10 +591,15 @@ describe('task admission and package contracts', () => {
|
||||
)
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
resolveExecutable.mockRejectedValueOnce(new Error('claude missing from PATH'))
|
||||
vi.stubEnv('PATH', '/host/bin')
|
||||
queryMock.mockImplementationOnce(() => {
|
||||
throw new Error(
|
||||
'Native CLI binary for fixture-platform not found. Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set options.pathToClaudeCodeExecutable.',
|
||||
)
|
||||
})
|
||||
await expect(ctx.subagents.start('claude-diagnostic', request()))
|
||||
.rejects.toThrow('claude missing from PATH')
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
.rejects.toThrow('Native CLI binary for fixture-platform not found')
|
||||
expect(resolveExecutable).not.toHaveBeenCalled()
|
||||
|
||||
const run = await ctx.subagents.start('claude-diagnostic', request())
|
||||
child.settle({ exitCode: 9, signal: null })
|
||||
@@ -530,14 +611,10 @@ describe('task admission and package contracts', () => {
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
'subagent-claude-code "claude-diagnostic": child run failed (error):',
|
||||
))
|
||||
expect(resolveExecutable).toHaveBeenCalledWith(
|
||||
'claude',
|
||||
expect.objectContaining({ ANTHROPIC_API_KEY: 'provider-fake-key' }),
|
||||
expect.any(AbortSignal),
|
||||
)
|
||||
expect(queryMock.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable)
|
||||
.toBe('/native/claude')
|
||||
expect(queryMock.mock.calls[0]?.[0].options.permissionMode).toBe('auto')
|
||||
expect(resolveExecutable).not.toHaveBeenCalled()
|
||||
expect(queryMock.mock.calls[1]?.[0].options)
|
||||
.not.toHaveProperty('pathToClaudeCodeExecutable')
|
||||
expect(queryMock.mock.calls[1]?.[0].options.permissionMode).toBe('auto')
|
||||
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cwd: process.cwd(),
|
||||
graceMs: 29,
|
||||
@@ -617,20 +694,17 @@ describe('official spawn projection', () => {
|
||||
)).toThrow('SDK spawn request omitted its workspace')
|
||||
})
|
||||
|
||||
it.each(['cmd', 'bat'])('routes a Windows .%s shim through cmd.exe', (extension) => {
|
||||
const command = String.raw`C:\Program Files\Claude\claude.${extension}`
|
||||
it('forwards the SDK-selected Windows native executable without a batch shim', () => {
|
||||
const command = String.raw`C:\Program Files\Claude\claude.exe`
|
||||
const spec = claudeSpawnSpec(sdkSpawnOptions({
|
||||
command,
|
||||
args: ['--output-format', 'stream-json'],
|
||||
}), 7, 'win32')
|
||||
}), 7)
|
||||
|
||||
expect(spec.argv).toEqual([
|
||||
'cmd.exe', '/d', '/v:off', '/s', '/c', '%DSH_CLAUDE_CODE_EXECUTABLE%',
|
||||
'--output-format', 'stream-json',
|
||||
command, '--output-format', 'stream-json',
|
||||
])
|
||||
expect(spec.env).toEqual(expect.objectContaining({
|
||||
DSH_CLAUDE_CODE_EXECUTABLE: `"${command}"`,
|
||||
}))
|
||||
expect(spec.env).not.toHaveProperty('DSH_CLAUDE_CODE_EXECUTABLE')
|
||||
})
|
||||
|
||||
it('projects streams, exit facts, listeners, and idempotent tree termination', async () => {
|
||||
@@ -701,7 +775,6 @@ describe('query options and result mapping', () => {
|
||||
const diagnostics: string[] = []
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: 'acceptEdits',
|
||||
env: {
|
||||
HOST_VISIBLE: 'overridden',
|
||||
@@ -723,12 +796,12 @@ describe('query options and result mapping', () => {
|
||||
expect(options).toMatchObject({
|
||||
abortController: controller,
|
||||
cwd: '/workspace',
|
||||
pathToClaudeCodeExecutable: '/native/claude',
|
||||
persistSession: false,
|
||||
disallowedTools: ['AskUserQuestion'],
|
||||
permissionMode: 'acceptEdits',
|
||||
supportedDialogKinds: ['refusal_fallback_prompt'],
|
||||
})
|
||||
expect(options).not.toHaveProperty('pathToClaudeCodeExecutable')
|
||||
expect(options).not.toHaveProperty('allowDangerouslySkipPermissions')
|
||||
expect(options.env).toMatchObject({
|
||||
HOST_VISIBLE: 'overridden',
|
||||
@@ -792,7 +865,6 @@ describe('query options and result mapping', () => {
|
||||
const child = fakeChild()
|
||||
const options = claudeQueryOptions({
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode,
|
||||
env: {},
|
||||
disposeGraceMs: 17,
|
||||
@@ -816,7 +888,6 @@ describe('query options and result mapping', () => {
|
||||
const child = fakeChild()
|
||||
const options = claudeQueryOptions({
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: 'plan',
|
||||
env: {},
|
||||
disposeGraceMs: 17,
|
||||
@@ -942,7 +1013,6 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
let childIndex = 0
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: 'dontAsk',
|
||||
env: {},
|
||||
disposeGraceMs: 5,
|
||||
@@ -1012,7 +1082,6 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
let index = 0
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: 'dontAsk',
|
||||
env: {},
|
||||
disposeGraceMs: 5,
|
||||
@@ -1064,7 +1133,6 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
request(undefined, parentAbort.signal),
|
||||
{
|
||||
cwd: '/workspace',
|
||||
executable: '/native/claude',
|
||||
permissionMode: DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
|
||||
env: {},
|
||||
disposeGraceMs: 5,
|
||||
@@ -1153,14 +1221,101 @@ describe('run publication, cancellation, and settlement', () => {
|
||||
expect(factoryController?.signal.aborted).toBe(true)
|
||||
expect(spawned.terminate).toHaveBeenCalledOnce()
|
||||
|
||||
const spawnError = Object.assign(
|
||||
new Error('spawn /sdk/claude EACCES'),
|
||||
{ code: 'EACCES', path: '/sdk/claude' },
|
||||
)
|
||||
const failedSpawn = fakeChild({
|
||||
pid: -1,
|
||||
doneError: new Error('spawn failed'),
|
||||
doneError: spawnError,
|
||||
})
|
||||
const failed = fakeRun([], undefined, failedSpawn)
|
||||
await expect(startClaudeCodeRun(request(), failed.spec))
|
||||
.rejects.toBeInstanceOf(AggregateError)
|
||||
.rejects.toBe(spawnError)
|
||||
expect(failed.close).toHaveBeenCalledOnce()
|
||||
expect(failedSpawn.terminate).not.toHaveBeenCalled()
|
||||
expect(failedSpawn.waitForExit).not.toHaveBeenCalled()
|
||||
|
||||
const failedSpawnAbort = new AbortController()
|
||||
const cancelledFailedSpawn = fakeChild({
|
||||
pid: -1,
|
||||
doneError: spawnError,
|
||||
})
|
||||
const cancelledFailedClose = vi.fn()
|
||||
queryMock.mockImplementationOnce(({ options }) => {
|
||||
options.spawnClaudeCodeProcess!(sdkSpawnOptions())
|
||||
failedSpawnAbort.abort(new Error('startup cancelled'))
|
||||
return queryFrom([], undefined, cancelledFailedClose)
|
||||
})
|
||||
await expect(startClaudeCodeRun(
|
||||
request(undefined, failedSpawnAbort.signal),
|
||||
{ ...unused.spec, spawn: () => cancelledFailedSpawn.handle },
|
||||
)).rejects.toThrow('aborted before SDK startup')
|
||||
expect(cancelledFailedClose).toHaveBeenCalledOnce()
|
||||
|
||||
const cancelledFailedSpawnCloseError = new Error('cancelled query close failed')
|
||||
const cancelledFailedSpawnClose = vi.fn(() => {
|
||||
throw cancelledFailedSpawnCloseError
|
||||
})
|
||||
const cancelledFailedSpawnWithCloseFailure = fakeChild({
|
||||
pid: -1,
|
||||
doneError: spawnError,
|
||||
})
|
||||
const failedSpawnAbortWithCloseFailure = new AbortController()
|
||||
queryMock.mockImplementationOnce(({ options }) => {
|
||||
options.spawnClaudeCodeProcess!(sdkSpawnOptions())
|
||||
failedSpawnAbortWithCloseFailure.abort(new Error('startup cancelled'))
|
||||
return queryFrom([], undefined, cancelledFailedSpawnClose)
|
||||
})
|
||||
const cancelledWithCloseFailure = startClaudeCodeRun(
|
||||
request(undefined, failedSpawnAbortWithCloseFailure.signal),
|
||||
{ ...unused.spec, spawn: () => cancelledFailedSpawnWithCloseFailure.handle },
|
||||
)
|
||||
await expect(cancelledWithCloseFailure).rejects.toMatchObject({
|
||||
message: 'subagent-claude-code: request was aborted before SDK startup; Claude Code process startup also failed: spawn /sdk/claude EACCES; query cleanup also failed',
|
||||
errors: [
|
||||
expect.objectContaining({ message: 'subagent-claude-code: request was aborted before SDK startup' }),
|
||||
spawnError,
|
||||
cancelledFailedSpawnCloseError,
|
||||
],
|
||||
})
|
||||
expect(cancelledFailedSpawnClose).toHaveBeenCalledOnce()
|
||||
|
||||
const failedSpawnCloseError = new Error('query close failed')
|
||||
const failedSpawnClose = vi.fn(() => { throw failedSpawnCloseError })
|
||||
const failedSpawnWithCloseFailure = fakeChild({
|
||||
pid: -1,
|
||||
doneError: spawnError,
|
||||
})
|
||||
queryMock.mockImplementationOnce(({ options }) => {
|
||||
options.spawnClaudeCodeProcess!(sdkSpawnOptions())
|
||||
return queryFrom([], undefined, failedSpawnClose)
|
||||
})
|
||||
const failedWithCloseFailure = startClaudeCodeRun(request(), {
|
||||
...unused.spec,
|
||||
spawn: () => failedSpawnWithCloseFailure.handle,
|
||||
})
|
||||
await expect(failedWithCloseFailure)
|
||||
.rejects.toThrow('spawn /sdk/claude EACCES')
|
||||
await expect(failedWithCloseFailure).rejects.toMatchObject({
|
||||
errors: [spawnError, failedSpawnCloseError],
|
||||
})
|
||||
|
||||
const cleanupError = new Error('live child cleanup failed')
|
||||
const constructionError = new Error(
|
||||
'query construction failed with a live child',
|
||||
)
|
||||
const liveChildCleanupFailure = fakeChild({ doneError: cleanupError })
|
||||
queryMock.mockImplementationOnce(({ options }) => {
|
||||
options.spawnClaudeCodeProcess!(sdkSpawnOptions())
|
||||
throw constructionError
|
||||
})
|
||||
await expect(startClaudeCodeRun(request(), {
|
||||
...unused.spec,
|
||||
spawn: () => liveChildCleanupFailure.handle,
|
||||
})).rejects.toMatchObject({
|
||||
errors: [constructionError, cleanupError],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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/subagent/subagent-codex/README.md
|
||||
README.md: 85358a3fbbab216bccccb3340be47a1c5b1433ef
|
||||
README.zh.md: 03b74233f18d55c7fa81b327e2de96cf85b816cc
|
||||
README.md: 1b9f777c26fa2e1ee050dfc73bdb648e66e93043
|
||||
README.zh.md: f70510dd680a78193ebb45e95b9e666de59db571
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
This package registers a Profile-named Codex subagent provider whose default name is `codex`. Each accepted run starts the official `codex app-server --stdio` command in the delegating Session's workspace, creates one ephemeral Codex thread, submits one self-contained text task, and returns either the selected final answer or safe failure detail through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
This package registers a Profile-named Codex subagent provider whose default name is `codex`. Each accepted run starts the official package-local Codex wrapper with `app-server --stdio` in the delegating Session's workspace, creates one ephemeral Codex thread, submits one self-contained text task, and returns either the selected final answer or a separate safe failure diagnostic through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
|
||||
## Start and ownership
|
||||
|
||||
@@ -33,9 +33,17 @@ The provider advertises no optional start-time capabilities and reports `inherit
|
||||
| `approve-for-me` | `approvalPolicy: on-request`, `approvalsReviewer: auto_review`, `sandbox: workspace-write` | Route permission requests through Codex automatic review without a human. |
|
||||
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`, `sandbox: danger-full-access` | Skip approval and sandbox enforcement; this value must be selected explicitly. |
|
||||
|
||||
Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. The Provider overrides only the selected thread approval/reviewer/sandbox fields; all other `CODEX_HOME`, project, model, provider, MCP, hook, skill, and account settings remain native. The plugin does not install Codex, select a model, create `CODEX_HOME`, log in, or probe a version. Credential-shaped ambient variables are removed by the subprocess seam, so an API key intended for the child must be supplied explicitly in `env`; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
|
||||
Production resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` dependency and launches that JavaScript wrapper with the current Node executable. The wrapper selects the matching native platform payload; the provider neither inspects nor falls back to a host `codex` on `PATH`. Native Codex configuration and authentication remain authoritative through the parent cwd, `HOME`, and `CODEX_HOME`, while the Provider overrides only the selected thread approval/reviewer/sandbox fields. All other project, model, provider, MCP, hook, skill, and account settings remain native. The plugin does not select a model, create `CODEX_HOME`, log in, or probe an account. Credential-shaped ambient variables are removed by the subprocess seam before the explicit `env` overlay is applied.
|
||||
|
||||
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-codex` and may mount one or more host-plane rows with distinct `providerName`, `permissionMode`, and `env` values; omitting `providerName` keeps the `codex` default. Loading an instance starts no Codex process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; installation brings the official wrapper and one compatible native platform payload into that Profile, while the declared `cordis.patch.yml` layer registers only the dormant `codex` Host provider and starts no Codex process. Removing the package withdraws that provider and its private runtime closure on the next Profile start.
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh --profile <name>
|
||||
```
|
||||
|
||||
Installation controls Host availability, not model permission. The Bundle supplies the dormant default `codex` row; the Profile may replace that row's complete config or mount additional rows with distinct `providerName`, `permissionMode`, and `env` values. Loading an instance starts no Codex process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
|
||||
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider and tool rows, and does not mount duplicate Job services.
|
||||
|
||||
@@ -55,7 +63,9 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
permissionMode: dangerously-bypass-approvals-and-sandbox
|
||||
env:
|
||||
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
|
||||
```
|
||||
|
||||
```yaml
|
||||
- id: jobs
|
||||
name: '@deepseek-ai/dsh-jobs-local'
|
||||
|
||||
@@ -64,6 +74,7 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
|
||||
- id: tool-subagent-codex-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
config:
|
||||
provider: codex-safe
|
||||
toolName: subagent_codex_safe
|
||||
@@ -81,7 +92,13 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
|
||||
## Product compatibility and evidence
|
||||
|
||||
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`; the npm package is a test-only dependency, and deployments still supply `codex` on `PATH`. Real-product coverage proves that two named instances retain separate environments and native modes, thread-level `never` overrides an ambient `on-request`, automatic review starts through the official app-server, dangerous bypass writes only in suite-owned temporary storage, safe diagnostics exclude raw commands and paths, and every wrapper/native process exits.
|
||||
The production wire intentionally implements only the app-server methods required by this one-shot contract. The runtime dependency and all six optional-dependency aliases are pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`. A normal install selects one payload for the current OS and CPU. For the current darwin-arm64 payload, `npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` reports 111,199,052 packed bytes and 274,777,843 unpacked bytes. That package contains native `codex`, `codex-code-mode-host`, `rg`, and `zsh` resources; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test drives the package wrapper against a loopback Responses fixture, observes the package-local argv, and proves wrapper and native descendants become quiescent.
|
||||
|
||||
Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload makes the first delegation fail with the wrapper's native-payload startup error. The provider neither probes a host CLI nor retries with one.
|
||||
|
||||
Real-product coverage additionally proves that thread-level `never` overrides an ambient `on-request`, automatic review starts through the official app-server, dangerous bypass writes only in suite-owned temporary storage, safe diagnostics exclude raw commands and paths, and every wrapper/native process exits.
|
||||
|
||||
The same real-product tier proves that two named instances retain separate environments and native modes.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -89,7 +106,7 @@ The production wire intentionally implements only the app-server methods require
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd; its model, system instructions, tools, and authentication come from the native Codex installation and configuration, while the selected Provider instance's Profile configuration fixes the thread's environment, non-interactive approval policy, and sandbox mode.
|
||||
The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd; its model, system instructions, tools, and authentication come from native Codex configuration, the selected Provider instance's Profile configuration fixes the thread's environment, non-interactive approval policy, and sandbox mode, and the executable version comes from the Bundle's pinned platform payload.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -117,7 +134,8 @@ Append-only: foreground adds one result after the reusable parent prefix, while
|
||||
|
||||
- **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
|
||||
- **Static instance selection** — Profile rows fix provider names and tool bindings; calls cannot choose a provider dynamically, and every exposed tool needs a unique `toolName`.
|
||||
- **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate.
|
||||
- **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, trust a project, or rewrite Codex settings; configuration and authentication failures surface as startup or run errors.
|
||||
- **The native platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first run; there is no host-CLI fallback.
|
||||
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
|
||||
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; the three Profile modes never create a DSH interaction channel or per-call allow policy.
|
||||
- **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, commentary, intermediate messages, tool traffic, usage, raw stderr, and workspace diffs remain outside the parent Session, while generic Job ids, notices, and status come from the shared job runtime.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
本包注册由 Profile 命名、默认名称为 `codex` 的 Codex subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中启动官方 `codex app-server --stdio` 命令,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回选定的最终答案或安全失败说明。
|
||||
本包注册由 Profile 命名、默认名称为 `codex` 的 Codex subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中使用 `app-server --stdio` 启动官方包内 Codex wrapper,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回选定的最终答案或独立的安全失败诊断。
|
||||
|
||||
## 启动与所有权
|
||||
|
||||
@@ -33,9 +33,17 @@
|
||||
| `approve-for-me` | `approvalPolicy: on-request`、`approvalsReviewer: auto_review`、`sandbox: workspace-write` | 由 Codex 自动评审权限请求,不等待人工。 |
|
||||
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`、`sandbox: danger-full-access` | 跳过审批与 sandbox;必须显式选择该值。 |
|
||||
|
||||
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。提供方只覆盖选定线程的 approval/reviewer/sandbox 字段;其他 `CODEX_HOME`、项目、模型、provider、MCP、hook、skill 与账户设置仍由原生机制负责。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
|
||||
生产环境会解析锁定的 `@openai/codex@0.147.0` 依赖所声明的 `codex` bin,并使用当前 Node 可执行文件启动该 JavaScript wrapper。Wrapper 会选择匹配的原生平台载荷;提供方既不检查也不回退 `PATH` 中的宿主 `codex`。父会话 cwd、`HOME` 与 `CODEX_HOME` 继续让原生 Codex 配置和身份验证保持权威,而提供方只覆盖选定线程的 approval/reviewer/sandbox 字段。其他项目、模型、provider、MCP、hook、skill 与账户设置仍由原生机制负责。本插件不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测账户。子进程 seam 会先移除具有凭证特征的环境变量,再应用显式 `env` 覆盖。
|
||||
|
||||
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-codex`,并可在 host plane(宿主平面)挂载一个或多个具有不同 `providerName`、`permissionMode` 与 `env` 的配置项;省略 `providerName` 时仍使用默认的 `codex`。加载实例本身不会在绑定工具调用前启动 Codex 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;安装会把官方 wrapper 与一个兼容的原生平台载荷带入该 Profile,而包所声明的 `cordis.patch.yml` 层只注册休眠的 `codex` Host provider,不会启动 Codex 进程。移除该包后,下一次 Profile 启动会撤回这一 provider 及其私有运行时闭包。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile <name> add @deepseek-ai/dsh-subagent-codex
|
||||
dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
|
||||
dsh --profile <name>
|
||||
```
|
||||
|
||||
安装决定 Host 可用性,而不是模型权限。Bundle 会提供休眠的默认 `codex` 配置项;Profile 可以替换该配置项的完整 config,也可以挂载更多具有不同 `providerName`、`permissionMode` 与 `env` 的配置项。加载实例本身不会在绑定工具调用前启动 Codex 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
|
||||
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 配置项,新增产品提供方与工具配置项,而且不重复挂载 Job 服务。
|
||||
|
||||
@@ -55,7 +63,9 @@
|
||||
permissionMode: dangerously-bypass-approvals-and-sandbox
|
||||
env:
|
||||
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
|
||||
```
|
||||
|
||||
```yaml
|
||||
- id: jobs
|
||||
name: '@deepseek-ai/dsh-jobs-local'
|
||||
|
||||
@@ -64,6 +74,7 @@
|
||||
|
||||
- id: tool-subagent-codex-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
disabled: true
|
||||
config:
|
||||
provider: codex-safe
|
||||
toolName: subagent_codex_safe
|
||||
@@ -81,7 +92,13 @@
|
||||
|
||||
## 产品兼容性与证据
|
||||
|
||||
生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.147.0` / `codex-cli 0.147.0`;该 NPM 包仅作为测试依赖,部署环境仍需通过 `PATH` 提供 `codex`。真实产品覆盖会证明两个命名实例保留彼此独立的环境与原生模式,线程级 `never` 覆盖环境中的 `on-request`,自动评审通过官方 app-server 启动,危险绕过只在测试拥有的临时存储中写入,安全诊断不包含原始命令与路径,而且所有 wrapper/native 进程都会退出。
|
||||
生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。运行时依赖与六个 optional-dependency alias 均锁定到 `@openai/codex@0.147.0` / `codex-cli 0.147.0`。普通安装会按当前操作系统与 CPU 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` 报告压缩包为 111,199,052 字节、解包后为 274,777,843 字节。该包包含原生 `codex`、`codex-code-mode-host`、`rg` 与 `zsh` 资源;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会驱动包内 wrapper 连接回环 Responses fixture,观测包内 argv,并证明 wrapper 与原生后代进程完全停稳。
|
||||
|
||||
如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,第一次委派会以 wrapper 的原生载荷启动错误失败。提供方既不会探测宿主 CLI,也不会用它重试。
|
||||
|
||||
真实产品覆盖还会证明线程级 `never` 覆盖环境中的 `on-request`,自动评审通过官方 app-server 启动,危险绕过只在测试拥有的临时存储中写入,安全诊断不包含原始命令与路径,而且所有 wrapper/native 进程都会退出。
|
||||
|
||||
同一真实产品层级还会证明两个命名实例保留彼此独立的环境与原生模式。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -89,7 +106,7 @@
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具和身份验证来自原生 Codex 安装与配置,而所选提供方实例的 Profile 配置会固定该线程的环境、非交互审批策略与沙箱模式。
|
||||
Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具和身份验证来自原生 Codex 配置,所选提供方实例的 Profile 配置会固定该线程的环境、非交互审批策略与沙箱模式,而可执行版本来自 Bundle 锁定的平台载荷。
|
||||
|
||||
#### 对 token 的影响
|
||||
|
||||
@@ -117,7 +134,8 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
|
||||
|
||||
- **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。
|
||||
- **静态选择实例**:Profile 配置项固定提供方名称与工具绑定;调用无法动态选择提供方,而且每个公开工具都需要唯一的 `toolName`。
|
||||
- **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
|
||||
- **身份验证与账户状态仍由原生机制管理**:Bundle 会提供 CLI,但不会创建账户、登录、信任项目或改写 Codex 设置;配置与身份验证失败会呈现为启动错误或运行错误。
|
||||
- **委派时必须存在原生平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次运行时失败;不会回退到宿主 CLI。
|
||||
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
|
||||
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;三种 Profile 模式都不会创建 DSH 交互通道或逐次调用 allow 策略。
|
||||
- **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、过程说明、中间消息、工具通信、用量信息、原始 stderr 和工作区差异不会进入父会话,通用 Job id、通知与状态来自共享作业运行时。
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Optional Profile Bundle: register the Codex provider on the Host plane only.
|
||||
# Agent Presets grant the model-facing tool independently.
|
||||
- insert:
|
||||
- id: subagent-codex
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
@@ -28,13 +28,18 @@
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"cordis.patch.yml",
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dsh": {
|
||||
"bundle": {
|
||||
"patch": "./cordis.patch.yml"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
@@ -42,7 +47,9 @@
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepseek-ai/schemastery": "workspace:^"
|
||||
"@deepseek-ai/dsh-sdk-protocol": "workspace:^",
|
||||
"@deepseek-ai/schemastery": "workspace:^",
|
||||
"@openai/codex": "0.147.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||
@@ -50,13 +57,11 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
|
||||
"@deepseek-ai/dsh-sdk-protocol": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"@openai/codex": "0.147.0",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Profile-named Codex one-shot subagent provider. Every accepted run starts a
|
||||
* fresh official `codex app-server --stdio` process in the delegating
|
||||
* Session's workspace and publishes only after an ephemeral thread exists.
|
||||
* fresh official package-local Codex wrapper with `app-server --stdio` in the
|
||||
* delegating Session's workspace and publishes only after an ephemeral thread exists.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-codex
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
@@ -24,6 +26,42 @@ import { CodexAppServerWire } from './wire.ts'
|
||||
|
||||
/** Default POSIX grace between subprocess termination tiers. */
|
||||
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
|
||||
/** Bounded stderr tail retained only to recognize the wrapper's payload error. */
|
||||
const CODEX_STDERR_TAIL_BYTES = 16 * 1024
|
||||
|
||||
interface CodexPackageManifest {
|
||||
readonly bin: {
|
||||
readonly codex: string
|
||||
}
|
||||
}
|
||||
|
||||
const codexPackageJsonPath = createRequire(import.meta.url).resolve('@openai/codex/package.json')
|
||||
const codexPackageManifest = JSON.parse(
|
||||
readFileSync(codexPackageJsonPath, 'utf8'),
|
||||
) as CodexPackageManifest
|
||||
|
||||
/** Absolute package-local JavaScript wrapper selected by the package manifest. */
|
||||
const CODEX_PACKAGE_BIN = resolve(
|
||||
dirname(codexPackageJsonPath),
|
||||
codexPackageManifest.bin.codex,
|
||||
)
|
||||
|
||||
function missingPayloadDiagnostic(stderr: string): string | undefined {
|
||||
const platformPackage = /Missing optional dependency (@openai\/codex-[a-z0-9-]+)/
|
||||
.exec(stderr)?.[1]
|
||||
return platformPackage === undefined
|
||||
? undefined
|
||||
: `Missing optional dependency ${platformPackage}`
|
||||
}
|
||||
|
||||
function withMissingPayloadDiagnostic(
|
||||
error: Error,
|
||||
stderr: string,
|
||||
): Error {
|
||||
const diagnostic = missingPayloadDiagnostic(stderr)
|
||||
if (diagnostic === undefined || error.message.includes(diagnostic)) return error
|
||||
return new Error(`${error.message}: ${diagnostic}`, { cause: error })
|
||||
}
|
||||
|
||||
/** Profile-selectable non-interactive Codex permission mode. */
|
||||
export type CodexPermissionMode =
|
||||
@@ -42,20 +80,11 @@ export const CODEX_PERMISSION_MODES = [
|
||||
export const DEFAULT_CODEX_PERMISSION_MODE: CodexPermissionMode = 'never'
|
||||
|
||||
/**
|
||||
* Resolve the fixed app-server command for a platform.
|
||||
*
|
||||
* Windows npm and pnpm installs expose `codex.cmd`, which requires `cmd.exe`;
|
||||
* the argv is constant so no task or configuration text enters the
|
||||
* shell boundary.
|
||||
* @param platform - host platform used to select the executable boundary.
|
||||
* @returns argv for the fixed Codex app-server command.
|
||||
* Fixed package-local app-server command, independent of the host `PATH`.
|
||||
* @returns Node, the official wrapper, and the fixed app-server arguments.
|
||||
*/
|
||||
export function codexAppServerArgv(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string[] {
|
||||
return platform === 'win32'
|
||||
? ['cmd.exe', '/d', '/s', '/c', 'codex', 'app-server', '--stdio']
|
||||
: ['codex', 'app-server', '--stdio']
|
||||
export function codexAppServerArgv(): string[] {
|
||||
return [process.execPath, CODEX_PACKAGE_BIN, 'app-server', '--stdio']
|
||||
}
|
||||
|
||||
/** Fully resolved inputs for one Codex app-server run. */
|
||||
@@ -154,8 +183,13 @@ export async function startCodexRun(
|
||||
child.stdin as NonNullable<SubprocessHandle['stdin']>,
|
||||
spec.permissionMode,
|
||||
)
|
||||
let stderrTail = Buffer.alloc(0)
|
||||
const onStderr = (chunk: Buffer | string): void => {
|
||||
const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
|
||||
const combined = Buffer.concat([stderrTail, bytes])
|
||||
stderrTail = combined.length > CODEX_STDERR_TAIL_BYTES
|
||||
? Buffer.from(combined.subarray(combined.length - CODEX_STDERR_TAIL_BYTES))
|
||||
: combined
|
||||
wire.observeStderr(bytes.toString())
|
||||
try {
|
||||
// Synchronous fd forwarding preserves byte order without owning a
|
||||
@@ -174,6 +208,9 @@ export async function startCodexRun(
|
||||
const disposeProcess = async (): Promise<void> => {
|
||||
try {
|
||||
await disposeCodexChild(wire, child)
|
||||
// Let stderr already queued by the process close reach both bounded
|
||||
// diagnostic consumers before their listeners are detached.
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
} finally {
|
||||
child.stderr?.off('data', onStderr)
|
||||
child.stderr?.off('error', onStderrError)
|
||||
@@ -206,18 +243,19 @@ export async function startCodexRun(
|
||||
await Promise.race([wire.startThread(spec.cwd, request.signal), processFailure])
|
||||
} catch (error: unknown) {
|
||||
request.signal.removeEventListener('abort', onAbort)
|
||||
const startupCause = thrown(error)
|
||||
try {
|
||||
await disposeProcess()
|
||||
} catch (disposeError: unknown) {
|
||||
throw new AggregateError(
|
||||
[thrown(error), thrown(disposeError)],
|
||||
[withMissingPayloadDiagnostic(startupCause, stderrTail.toString()), thrown(disposeError)],
|
||||
'subagent-codex: startup failed and app-server cleanup also failed',
|
||||
)
|
||||
}
|
||||
if (runAbort.signal.aborted) {
|
||||
throw new Error('subagent-codex: request was aborted before run publication')
|
||||
}
|
||||
throw thrown(error)
|
||||
throw withMissingPayloadDiagnostic(startupCause, stderrTail.toString())
|
||||
}
|
||||
|
||||
const collectOutput = (): ContentBlock[] => wire.collectOutput()
|
||||
@@ -232,7 +270,7 @@ export async function startCodexRun(
|
||||
// Give stderr data already queued in Node one turn to reach the wire
|
||||
// before settlement snapshots the diagnostic; later OS data is best-effort.
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
throw error
|
||||
throw withMissingPayloadDiagnostic(thrown(error), stderrTail.toString())
|
||||
}
|
||||
},
|
||||
collectOutput,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
@@ -12,16 +13,24 @@ const fixtureDir = fileURLToPath(new URL(
|
||||
))
|
||||
const driver = join(fixtureDir, 'driver.ts')
|
||||
const configPath = join(fixtureDir, 'cordis.yml')
|
||||
const packageDir = fileURLToPath(new URL('..', import.meta.url))
|
||||
const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
|
||||
dsh?: { bundle?: { patch?: string } }
|
||||
}
|
||||
const bundlePatch = manifest.dsh?.bundle?.patch
|
||||
if (bundlePatch === undefined) throw new Error('Codex package must declare a Bundle patch')
|
||||
const bundlePatchPath = join(packageDir, bundlePatch)
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('Codex provider public Loader composition', () => {
|
||||
it('loads two named instances, their tools, and job controls without starting Codex', async () => {
|
||||
it('loads the Bundle default, two named instances, their tools, and job controls without starting Codex', async () => {
|
||||
const { stdout, stderr } = await runLoaderSmoke({
|
||||
label: 'subagent-codex Loader composition',
|
||||
tempDirPrefix: 'dsh-subagent-codex-loader-',
|
||||
binScript: driver,
|
||||
libBinScript: driver,
|
||||
configPath,
|
||||
binArgs: [configPath, bundlePatchPath],
|
||||
tsconfigPath: repoTsconfig,
|
||||
env: {
|
||||
// Loading the optional package must not probe or start a Codex binary.
|
||||
@@ -31,8 +40,18 @@ describe('Codex provider public Loader composition', () => {
|
||||
|
||||
expect(stderr).toBe('')
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
providers: ['codex-primary', 'codex-secondary'],
|
||||
providers: ['codex-primary', 'codex-secondary', 'codex'],
|
||||
providerDetails: [
|
||||
{
|
||||
name: 'codex',
|
||||
capabilities: {
|
||||
outputSchema: false,
|
||||
depthLimit: false,
|
||||
toolFilter: false,
|
||||
persona: false,
|
||||
},
|
||||
inheritsParentContext: false,
|
||||
},
|
||||
{
|
||||
name: 'codex-primary',
|
||||
capabilities: {
|
||||
@@ -55,6 +74,11 @@ describe('Codex provider public Loader composition', () => {
|
||||
},
|
||||
],
|
||||
tools: [
|
||||
{
|
||||
name: 'subagent_codex',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
{
|
||||
name: 'subagent_codex_primary',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -24,12 +24,12 @@ import {
|
||||
} from './deepseek-responses-bridge.ts'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
|
||||
const codexPackageJson = createRequire(import.meta.url).resolve('@openai/codex/package.json')
|
||||
const codexPackage = JSON.parse(readFileSync(
|
||||
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
|
||||
codexPackageJson,
|
||||
'utf8',
|
||||
)) as { version: string }
|
||||
)) as { version: string; bin: { codex: string } }
|
||||
const codexEntry = resolve(dirname(codexPackageJson), codexPackage.bin.codex)
|
||||
|
||||
const roots: string[] = []
|
||||
const contexts: Context[] = []
|
||||
@@ -88,7 +88,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||
CODEX_HOME: codexHome,
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: join(root, 'xdg-config'),
|
||||
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
|
||||
PATH: root,
|
||||
HTTP_PROXY: '',
|
||||
HTTPS_PROXY: '',
|
||||
ALL_PROXY: '',
|
||||
@@ -106,7 +106,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
|
||||
return handle
|
||||
})
|
||||
await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
|
||||
const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], {
|
||||
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
|
||||
env: { ...process.env, ...env },
|
||||
})
|
||||
expect(codexPackage.version).toBe('0.147.0')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
@@ -7,18 +8,16 @@ import {
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join, resolve } from 'node:path'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import * as codex from '../src/index.ts'
|
||||
import type { CodexPermissionMode } from '../src/run.ts'
|
||||
@@ -31,11 +30,13 @@ import {
|
||||
const execFileAsync = promisify(execFile)
|
||||
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
|
||||
const codexEntry = join(packageRoot, 'node_modules', '@openai', 'codex', 'bin', 'codex.js')
|
||||
const codexPackageJson = createRequire(import.meta.url).resolve('@openai/codex/package.json')
|
||||
const codexPackage = JSON.parse(readFileSync(
|
||||
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
|
||||
codexPackageJson,
|
||||
'utf8',
|
||||
)) as { version: string }
|
||||
)) as { version: string; bin: { codex: string } }
|
||||
const codexEntry = resolve(dirname(codexPackageJson), codexPackage.bin.codex)
|
||||
const codexPackageRoot = dirname(dirname(codexEntry))
|
||||
|
||||
const roots: string[] = []
|
||||
const fixtures: ResponsesFixture[] = []
|
||||
@@ -52,6 +53,7 @@ afterEach(async () => {
|
||||
interface RealHarness {
|
||||
readonly ctx: Context
|
||||
readonly handles: SubprocessHandle[]
|
||||
readonly spawnSpecs: SubprocessSpawnSpec[]
|
||||
readonly parent: Agent
|
||||
readonly env: Record<string, string>
|
||||
readonly workspace: string
|
||||
@@ -98,7 +100,7 @@ async function realInstanceFixture(
|
||||
CODEX_HOME: codexHome,
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: join(root, 'xdg'),
|
||||
PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
|
||||
PATH: root,
|
||||
HTTP_PROXY: '',
|
||||
HTTPS_PROXY: '',
|
||||
ALL_PROXY: '',
|
||||
@@ -138,7 +140,7 @@ async function realHarness(
|
||||
readonly fixture: ResponsesFixture
|
||||
}> {
|
||||
const instance = await realInstanceFixture(script)
|
||||
const { ctx, handles } = await realRuntime()
|
||||
const { ctx, handles, spawnSpecs } = await realRuntime()
|
||||
await ctx.plugin(codex, {
|
||||
env: instance.env,
|
||||
...permissionMode === undefined ? {} : { permissionMode },
|
||||
@@ -152,6 +154,7 @@ async function realHarness(
|
||||
harness: {
|
||||
ctx,
|
||||
handles,
|
||||
spawnSpecs,
|
||||
parent,
|
||||
env: instance.env,
|
||||
workspace: instance.workspace,
|
||||
@@ -210,6 +213,13 @@ describe('real @openai/codex 0.147.0 product', () => {
|
||||
})
|
||||
await run.dispose()
|
||||
|
||||
expect(harness.spawnSpecs[0]?.argv).toEqual([
|
||||
process.execPath,
|
||||
codexEntry,
|
||||
'app-server',
|
||||
'--stdio',
|
||||
])
|
||||
|
||||
expect(fixture.requests).toHaveLength(1)
|
||||
const recorded = fixture.requests[0]!
|
||||
expect(recorded.method).toBe('POST')
|
||||
@@ -219,6 +229,24 @@ describe('real @openai/codex 0.147.0 product', () => {
|
||||
await expectQuiescent(harness.handles)
|
||||
}, 60_000)
|
||||
|
||||
it('fails a missing platform payload without falling back to a host codex', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-codex-missing-payload-'))
|
||||
roots.push(root)
|
||||
const isolatedPackage = join(root, 'node_modules', '@openai', 'codex')
|
||||
mkdirSync(dirname(isolatedPackage), { recursive: true })
|
||||
cpSync(codexPackageRoot, isolatedPackage, { recursive: true, dereference: true })
|
||||
const isolatedEntry = join(isolatedPackage, 'bin', 'codex.js')
|
||||
|
||||
await expect(execFileAsync(process.execPath, [isolatedEntry, '--version'], {
|
||||
env: {
|
||||
PATH: codexBinDir,
|
||||
...process.platform === 'win32' && process.env.SystemRoot !== undefined
|
||||
? { SystemRoot: process.env.SystemRoot }
|
||||
: {},
|
||||
},
|
||||
})).rejects.toThrow(/Missing optional dependency @openai\/codex-[a-z0-9-]+/)
|
||||
}, 30_000)
|
||||
|
||||
it('runs two named instances concurrently and unloads one without revoking its run', async () => {
|
||||
const safeInstance = await realInstanceFixture([{ kind: 'hold' }])
|
||||
const bypassInstance = await realInstanceFixture([{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
@@ -61,6 +65,16 @@ vi.mock('node:fs', async (importOriginal) => {
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
|
||||
const CODEX_VERSION = '0.147.0'
|
||||
const CODEX_PLATFORM_PACKAGES = [
|
||||
'@openai/codex-darwin-arm64',
|
||||
'@openai/codex-darwin-x64',
|
||||
'@openai/codex-linux-arm64',
|
||||
'@openai/codex-linux-x64',
|
||||
'@openai/codex-win32-arm64',
|
||||
'@openai/codex-win32-x64',
|
||||
] as const
|
||||
|
||||
const fakeParent = {
|
||||
id: 'parent',
|
||||
session: { header: { cwd: process.cwd() } },
|
||||
@@ -139,6 +153,7 @@ interface FakeChild {
|
||||
readonly stderr: PassThrough
|
||||
readonly settle: (outcome?: SubprocessOutcome) => void
|
||||
readonly fail: (error: Error) => void
|
||||
readonly setStderr: (text: string) => void
|
||||
readonly terminate: () => void
|
||||
readonly waitForExit: (signal?: AbortSignal) => Promise<boolean>
|
||||
}
|
||||
@@ -210,6 +225,7 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
|
||||
stderr,
|
||||
settle,
|
||||
fail,
|
||||
setStderr: (text: string): void => { stderr.write(text) },
|
||||
terminate,
|
||||
waitForExit,
|
||||
}
|
||||
@@ -307,19 +323,66 @@ function turnCompleted(
|
||||
}
|
||||
|
||||
describe('task admission and package contracts', () => {
|
||||
it('keeps the app-server command fixed on POSIX and Windows', () => {
|
||||
expect(codexAppServerArgv('linux')).toEqual([
|
||||
'codex', 'app-server', '--stdio',
|
||||
])
|
||||
expect(codexAppServerArgv('win32')).toEqual([
|
||||
'cmd.exe',
|
||||
'/d',
|
||||
'/s',
|
||||
'/c',
|
||||
'codex',
|
||||
it('ships one independently installable provider-only Bundle patch', () => {
|
||||
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
|
||||
dependencies?: Record<string, string>
|
||||
files?: string[]
|
||||
dsh?: { bundle?: { patch?: string } }
|
||||
}
|
||||
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
|
||||
expect(manifest.files).toContain('cordis.patch.yml')
|
||||
expect(manifest.dependencies).toHaveProperty(
|
||||
'@deepseek-ai/dsh-sdk-protocol',
|
||||
'workspace:^',
|
||||
)
|
||||
expect(manifest.dependencies).toHaveProperty('@openai/codex', CODEX_VERSION)
|
||||
expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-claude-code')
|
||||
|
||||
const codexPackageJson = fileURLToPath(import.meta.resolve('@openai/codex/package.json'))
|
||||
const codexManifest = JSON.parse(readFileSync(codexPackageJson, 'utf8')) as {
|
||||
version: string
|
||||
bin: { codex: string }
|
||||
optionalDependencies: Record<string, string>
|
||||
}
|
||||
expect(codexManifest.version).toBe(CODEX_VERSION)
|
||||
expect(codexManifest.bin).toEqual({ codex: 'bin/codex.js' })
|
||||
expect(codexManifest.optionalDependencies).toEqual(Object.fromEntries(
|
||||
CODEX_PLATFORM_PACKAGES.map(packageName => [
|
||||
packageName,
|
||||
`npm:@openai/codex@${CODEX_VERSION}-${packageName.slice('@openai/codex-'.length)}`,
|
||||
]),
|
||||
))
|
||||
expect(codexAppServerArgv()).toEqual([
|
||||
process.execPath,
|
||||
resolve(dirname(codexPackageJson), codexManifest.bin.codex),
|
||||
'app-server',
|
||||
'--stdio',
|
||||
])
|
||||
|
||||
const lockfile = readFileSync(resolve(root, '../../../pnpm-lock.yaml'), 'utf8')
|
||||
for (const packageName of CODEX_PLATFORM_PACKAGES) {
|
||||
const suffix = packageName.slice('@openai/codex-'.length)
|
||||
expect(lockfile).toContain(` '@openai/codex@${CODEX_VERSION}-${suffix}':`)
|
||||
expect(lockfile).toContain(
|
||||
` '${packageName}': '@openai/codex@${CODEX_VERSION}-${suffix}'`,
|
||||
)
|
||||
}
|
||||
|
||||
const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'))
|
||||
const rows = Array.isArray(parsed)
|
||||
? (parsed as Array<{ insert?: Array<{ id?: string; name?: string }> }>).flatMap(entry => entry.insert ?? [])
|
||||
: []
|
||||
expect(rows).toEqual([{
|
||||
id: 'subagent-codex',
|
||||
name: '@deepseek-ai/dsh-subagent-codex',
|
||||
}])
|
||||
expect(JSON.stringify(rows)).not.toContain('tool-subagent')
|
||||
})
|
||||
|
||||
it('uses only the official package-declared wrapper for app-server', () => {
|
||||
expect(codexAppServerArgv()[0]).toBe(process.execPath)
|
||||
expect(codexAppServerArgv().slice(2)).toEqual(['app-server', '--stdio'])
|
||||
})
|
||||
|
||||
it('accepts one or more text blocks and rejects empty or non-text tasks', () => {
|
||||
@@ -1611,6 +1674,43 @@ describe('run lifecycle and quiescence', () => {
|
||||
expect(child.terminate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('surfaces only the wrapper missing-payload diagnostic during startup', async () => {
|
||||
const child = fakeChild()
|
||||
child.setStderr([
|
||||
`credential-like unrelated stderr ${'x'.repeat(16 * 1024)}`,
|
||||
'Error: Missing optional dependency @openai/codex-linux-x64. '
|
||||
+ 'Reinstall Codex: pnpm add -g @openai/codex@latest',
|
||||
].join('\n'))
|
||||
const starting = startCodexRun(request(), runSpec(child))
|
||||
child.settle({ exitCode: 1, signal: null })
|
||||
|
||||
const error: unknown = await starting.then(
|
||||
() => undefined,
|
||||
(failure: unknown) => failure,
|
||||
)
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
if (!(error instanceof Error)) throw new Error('expected startup failure')
|
||||
expect(error.message).toContain('Missing optional dependency @openai/codex-linux-x64')
|
||||
expect(error.message).not.toContain('credential-like unrelated stderr')
|
||||
expect(error.message).not.toContain('Reinstall Codex')
|
||||
expect(error.message).not.toContain('pnpm add -g')
|
||||
expect(child.terminate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('waits for process settlement before sampling the missing-payload diagnostic', async () => {
|
||||
const child = fakeChild({ exitOnTerminate: false })
|
||||
const starting = startCodexRun(request(), runSpec(child))
|
||||
child.fromChild.end()
|
||||
await vi.waitFor(() => { expect(child.terminate).toHaveBeenCalledTimes(1) })
|
||||
|
||||
child.setStderr('Error: Missing optional dependency @openai/codex-linux-x64.')
|
||||
child.settle({ exitCode: 1, signal: null })
|
||||
|
||||
await expect(starting).rejects.toThrow(
|
||||
'Missing optional dependency @openai/codex-linux-x64',
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps overlapping runs isolated', async () => {
|
||||
const initialStderrListeners = {
|
||||
error: process.stderr.listenerCount('error'),
|
||||
@@ -1752,7 +1852,7 @@ describe('run lifecycle and quiescence', () => {
|
||||
stopReason: 'error',
|
||||
})
|
||||
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
|
||||
argv: ['codex', 'app-server', '--stdio'],
|
||||
argv: codexAppServerArgv(),
|
||||
env: { OPENAI_API_KEY: 'fake' },
|
||||
graceMs: 25,
|
||||
cwd: process.cwd(),
|
||||
|
||||
Generated
+12
-6
@@ -7169,6 +7169,12 @@ importers:
|
||||
'@deepseek-ai/schemastery':
|
||||
specifier: link:../../../vendor/schemastery
|
||||
version: link:../../../vendor/schemastery
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0(zod@4.4.3)
|
||||
zod:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:^
|
||||
@@ -7203,9 +7209,15 @@ importers:
|
||||
|
||||
packages/subagent/subagent-codex:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-sdk-protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/protocol
|
||||
'@deepseek-ai/schemastery':
|
||||
specifier: link:../../../vendor/schemastery
|
||||
version: link:../../../vendor/schemastery
|
||||
'@openai/codex':
|
||||
specifier: 0.147.0
|
||||
version: 0.147.0
|
||||
devDependencies:
|
||||
'@deepseek-ai/cordis':
|
||||
specifier: workspace:^
|
||||
@@ -7225,9 +7237,6 @@ importers:
|
||||
'@deepseek-ai/dsh-loader-smoke':
|
||||
specifier: workspace:^
|
||||
version: link:../../test-support/loader-smoke
|
||||
'@deepseek-ai/dsh-sdk-protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../../sdk/protocol
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
@@ -7243,9 +7252,6 @@ importers:
|
||||
'@deepseek-ai/dsh-timeout':
|
||||
specifier: workspace:^
|
||||
version: link:../../util/timeout
|
||||
'@openai/codex':
|
||||
specifier: 0.147.0
|
||||
version: 0.147.0
|
||||
|
||||
packages/subagent/subagent-dsh-sdk:
|
||||
dependencies:
|
||||
|
||||
@@ -87,6 +87,11 @@ export interface PackageManifest {
|
||||
devDependencies?: Record<string, string>
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
dsh?: {
|
||||
bundle?: {
|
||||
patch?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One workspace manifest and its repo-relative path. */
|
||||
@@ -134,10 +139,6 @@ function workspaceManifests(): WorkspaceManifest[] {
|
||||
}
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
|
||||
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
|
||||
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
|
||||
// Statically linked client libraries keep their stylesheets next to the emitted
|
||||
// JavaScript, which imports them by relative path: the compile shell runs
|
||||
// them through its own CSS pipeline, so the sheets are published artifacts.
|
||||
@@ -164,7 +165,12 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
|
||||
}
|
||||
|
||||
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
|
||||
const declaredPatch = manifest.dsh?.bundle?.patch
|
||||
const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
|
||||
const extras = [
|
||||
...bundleFiles,
|
||||
...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
|
||||
]
|
||||
return [
|
||||
'lib/index.js',
|
||||
// Every package publishes its invariant ownership companion as a separate
|
||||
|
||||
@@ -673,7 +673,6 @@ export function render(): string {
|
||||
)
|
||||
? collectClaudeDistribution()
|
||||
: undefined
|
||||
|
||||
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
|
||||
// A copyleft license reaching a shipped surface is a distribution decision,
|
||||
// not a rendering detail; the notices cannot quietly absorb it.
|
||||
@@ -693,7 +692,7 @@ export function render(): string {
|
||||
|
||||
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
|
||||
|
||||
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('configuration source ownership gate', () => {
|
||||
it('rejects inline endpoints in shipped bundle patches', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-config-source-ownership-'))
|
||||
roots.push(root)
|
||||
const directory = join(root, 'packages/bundle/base')
|
||||
const directory = join(root, 'packages/subagent/subagent-claude-code')
|
||||
mkdirSync(directory, { recursive: true })
|
||||
writeFileSync(
|
||||
join(directory, 'cordis.patch.yml'),
|
||||
@@ -22,7 +22,7 @@ describe('configuration source ownership gate', () => {
|
||||
)
|
||||
|
||||
expect(collectConfigSourceOwnershipViolations(root)).toEqual([
|
||||
'packages/bundle/base/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
|
||||
'packages/subagent/subagent-claude-code/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
|
||||
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
|
||||
+ ' environment snapshot; inlining here bypasses both ladders.',
|
||||
])
|
||||
|
||||
@@ -14,7 +14,8 @@ const SHIPPED_CONFIG_GLOBS = [
|
||||
'apps/*/config/*.yml',
|
||||
'examples/*/*.cordis.yml',
|
||||
'examples/*/cordis.yml',
|
||||
'packages/bundle/*/cordis.patch.yml',
|
||||
// Bundle identity comes from the package manifest, not the domain directory.
|
||||
'packages/*/*/cordis.patch.yml',
|
||||
// The Python runtime ships its own default composition inside the wheel.
|
||||
'python/*/src/**/cordis.yml',
|
||||
]
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
* metadata field must stay static, and a disabled expression must parse.
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { metadataExpressionErrors } from './verify-cordis-config.ts'
|
||||
import {
|
||||
bundleManifestPaths,
|
||||
bundlePluginDependencyErrors,
|
||||
metadataExpressionErrors,
|
||||
} from './verify-cordis-config.ts'
|
||||
|
||||
describe('verify-cordis-config metadata expressions', () => {
|
||||
it('accepts a disabled !!js expression', () => {
|
||||
@@ -37,3 +44,45 @@ describe('verify-cordis-config metadata expressions', () => {
|
||||
expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('workspace Bundle discovery and product dependency closures', () => {
|
||||
it('discovers a Bundle outside packages/bundle from its manifest declaration', () => {
|
||||
const fixture = mkdtempSync(join(tmpdir(), 'dsh-bundle-discovery-'))
|
||||
try {
|
||||
const bundleDir = join(fixture, 'packages/subagent/example')
|
||||
const plainDir = join(fixture, 'packages/bundle/plain')
|
||||
mkdirSync(bundleDir, { recursive: true })
|
||||
mkdirSync(plainDir, { recursive: true })
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-subagent-example',
|
||||
dsh: { bundle: { patch: './cordis.patch.yml' } },
|
||||
}))
|
||||
writeFileSync(join(plainDir, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-plain',
|
||||
}))
|
||||
|
||||
expect(bundleManifestPaths(fixture)).toEqual([
|
||||
'packages/subagent/example/package.json',
|
||||
])
|
||||
} finally {
|
||||
rmSync(fixture, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('allows a Bundle to mount itself but rejects an undeclared plugin package', () => {
|
||||
const manifestPath = 'packages/subagent/example/package.json'
|
||||
const file = 'packages/subagent/example/cordis.patch.yml'
|
||||
const manifest = {
|
||||
name: '@deepseek-ai/dsh-subagent-example',
|
||||
dependencies: {},
|
||||
}
|
||||
const self = { file, name: '@deepseek-ai/dsh-subagent-example' }
|
||||
expect(bundlePluginDependencyErrors(manifestPath, manifest, [self])).toEqual([])
|
||||
expect(bundlePluginDependencyErrors(manifestPath, manifest, [
|
||||
self,
|
||||
{ file, name: '@deepseek-ai/dsh-missing-plugin' },
|
||||
])).toEqual([
|
||||
`${file}: @deepseek-ai/dsh-missing-plugin must be declared in ${manifestPath} dependencies`,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,12 +20,14 @@ interface JsExpr {
|
||||
__jsExpr: string
|
||||
}
|
||||
|
||||
interface PackageManifest {
|
||||
export interface PackageManifest {
|
||||
name?: string
|
||||
dependencies?: Record<string, string>
|
||||
optionalDependencies?: Record<string, string>
|
||||
dsh?: { bundle?: { patch?: string } }
|
||||
}
|
||||
|
||||
interface PluginReference {
|
||||
export interface PluginReference {
|
||||
file: string
|
||||
name: string
|
||||
}
|
||||
@@ -260,11 +262,14 @@ function validateExampleResolution(): string[] {
|
||||
|
||||
function validateAppResolution(): string[] {
|
||||
const violations: string[] = []
|
||||
const bundleManifests = bundleManifestPaths()
|
||||
// App overlays (and any config left under apps/cli/config) resolve from the
|
||||
// dsh app's own dependency surface — the profile module fallback mirrors it.
|
||||
const appDependencies = {
|
||||
...readManifest('apps/cli/package.json').dependencies,
|
||||
// The fallback also links every bundle's own dependencies (healProfilesModuleFallback).
|
||||
// The fallback also links every in-box bundle's own dependencies
|
||||
// (healProfilesModuleFallback). Optional Profile bundles stay outside the
|
||||
// app installation until that Profile installs them.
|
||||
...Object.fromEntries(globSync('packages/bundle/*/package.json', { cwd: root })
|
||||
.flatMap(file => Object.entries(readManifest(file).dependencies ?? {}))),
|
||||
}
|
||||
@@ -274,20 +279,49 @@ function validateAppResolution(): string[] {
|
||||
violations.push(...missingPluginDependencies(appReferences, appDependencies, 'apps/cli/package.json or a bundle manifest'))
|
||||
// Each bundle's patch rows must resolve from that bundle's own dependencies:
|
||||
// per-layer resolution anchors on the bundle package directory.
|
||||
for (const manifestPath of globSync('packages/bundle/*/package.json', { cwd: root })) {
|
||||
for (const manifestPath of bundleManifests) {
|
||||
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
|
||||
const manifest = readManifest(manifestPath)
|
||||
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
|
||||
violations.push(...missingPluginDependencies(
|
||||
// A bundle may mount its own package (the web-app runtime row).
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
|
||||
manifest.dependencies ?? {},
|
||||
manifestPath,
|
||||
))
|
||||
const patch = manifest.dsh?.bundle?.patch
|
||||
if (typeof patch !== 'string') continue
|
||||
const patchFile = relative(root, resolve(root, bundleDir, patch)).replaceAll('\\', '/')
|
||||
const references = pluginReferences.filter(reference => reference.file === patchFile)
|
||||
violations.push(...bundlePluginDependencyErrors(manifestPath, manifest, references))
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover workspace Bundle packages from their manifest declaration.
|
||||
* @param repoRoot Repository root to scan.
|
||||
* @returns Sorted repository-relative package manifest paths.
|
||||
*/
|
||||
export function bundleManifestPaths(repoRoot: string = root): string[] {
|
||||
return globSync('packages/*/*/package.json', { cwd: repoRoot })
|
||||
.filter(path => typeof readManifest(path, repoRoot).dsh?.bundle?.patch === 'string')
|
||||
.sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate plugin packages referenced by one Bundle patch.
|
||||
* @param manifestPath Repository-relative Bundle manifest path.
|
||||
* @param manifest Parsed Bundle manifest.
|
||||
* @param references Plugin rows read from the Bundle package directory.
|
||||
* @returns Missing production dependency diagnostics.
|
||||
*/
|
||||
export function bundlePluginDependencyErrors(
|
||||
manifestPath: string,
|
||||
manifest: PackageManifest,
|
||||
references: readonly PluginReference[],
|
||||
): string[] {
|
||||
return missingPluginDependencies(
|
||||
// A Bundle may mount its own package (for example, its provider or runtime row).
|
||||
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
|
||||
manifest.dependencies ?? {},
|
||||
manifestPath,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every configured specifier of a local workspace package must resolve through
|
||||
* the tsconfig `paths` facade to a `.ts`/`.tsx` source file. The `dsh` source
|
||||
@@ -363,8 +397,8 @@ function missingPluginDependencies(
|
||||
: `${[...locations].join(', ')}: ${packageName} must be declared in ${manifestPath} dependencies`)
|
||||
}
|
||||
|
||||
function readManifest(path: string): PackageManifest {
|
||||
return JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest
|
||||
function readManifest(path: string, repoRoot: string = root): PackageManifest {
|
||||
return JSON.parse(readFileSync(resolve(repoRoot, path), 'utf8')) as PackageManifest
|
||||
}
|
||||
|
||||
function localPackageDirectories(): Map<string, string> {
|
||||
|
||||
Reference in New Issue
Block a user