Ver Fonte

feat(subagent): make product providers directly installable

pku-xht há 1 mês atrás
pai
commit
d1629eed45
57 ficheiros alterados com 584 adições e 250 exclusões
  1. 2 2
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
  5. 9 11
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
  6. 9 11
      .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
  7. 2 2
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
  8. 3 3
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
  9. 3 3
      .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
  10. 2 2
      .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
  11. 7 3
      .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
  12. 7 3
      .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
  13. 2 0
      apps/cli/config/agent-presets/code/agent.cordis.yml
  14. 2 0
      apps/cli/config/agent-presets/cordis/agent.cordis.yml
  15. 9 2
      apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
  16. 2 0
      apps/cli/config/agent-presets/standard/agent.cordis.yml
  17. 2 2
      apps/cli/reference/README.i18n.yaml
  18. 12 0
      apps/cli/reference/README.md
  19. 12 0
      apps/cli/reference/README.zh.md
  20. 15 0
      apps/cli/tests/built-bin.e2e.ts
  21. 85 43
      apps/cli/tests/web-agent-presets.e2e.ts
  22. 2 2
      docs/module-graph.i18n.yaml
  23. 7 8
      docs/module-graph.md
  24. 7 8
      docs/module-graph.zh.md
  25. 2 16
      examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
  26. 22 35
      examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts
  27. 2 5
      examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
  28. 5 4
      examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
  29. 2 2
      packages/bundle/README.i18n.yaml
  30. 2 0
      packages/bundle/README.md
  31. 2 0
      packages/bundle/README.zh.md
  32. 2 2
      packages/bundle/base/README.i18n.yaml
  33. 1 2
      packages/bundle/base/README.md
  34. 1 2
      packages/bundle/base/README.zh.md
  35. 2 2
      packages/subagent/README.i18n.yaml
  36. 2 0
      packages/subagent/README.md
  37. 2 0
      packages/subagent/README.zh.md
  38. 2 2
      packages/subagent/subagent-claude-code/README.i18n.yaml
  39. 13 2
      packages/subagent/subagent-claude-code/README.md
  40. 13 2
      packages/subagent/subagent-claude-code/README.zh.md
  41. 6 0
      packages/subagent/subagent-claude-code/cordis.patch.yml
  42. 7 0
      packages/subagent/subagent-claude-code/package.json
  43. 27 37
      packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts
  44. 29 0
      packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
  45. 2 2
      packages/subagent/subagent-codex/README.i18n.yaml
  46. 13 2
      packages/subagent/subagent-codex/README.md
  47. 13 2
      packages/subagent/subagent-codex/README.zh.md
  48. 6 0
      packages/subagent/subagent-codex/cordis.patch.yml
  49. 8 2
      packages/subagent/subagent-codex/package.json
  50. 9 0
      packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
  51. 31 0
      packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
  52. 3 3
      pnpm-lock.yaml
  53. 2 0
      scripts/check-workspace-constraints.ts
  54. 2 2
      scripts/verify-config-source-ownership.spec.ts
  55. 2 1
      scripts/verify-config-source-ownership.ts
  56. 97 1
      scripts/verify-cordis-config.spec.ts
  57. 47 13
      scripts/verify-cordis-config.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
-2026-08-05-profile-plugin-bundles.md: ffef7b67a11617599674e0515e16bfd5283d8445
-2026-08-05-profile-plugin-bundles.zh.md: b190d5e834ba3ce8b719a4a3e61f256eb2c0d82b
+2026-08-05-profile-plugin-bundles.md: fdc92eec39b22a9d02258003fef2af9762c90eda
+2026-08-05-profile-plugin-bundles.zh.md: 51bb84d9b39e8aa51b6cc84f798bf512f8cca9a4

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md

@@ -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).
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md

@@ -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
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
-2026-08-10-product-subagent-providers-in-shared-host.md: 33b6eb6cf7a6c19e9ea71cdb7dc8881e8052ef24
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: fd78c7a3fee4e4ee30d27d87c752e1a23576fd85
+2026-08-10-product-subagent-providers-in-shared-host.md: 6db6ca665532ec2b457859243fee5cdc750e954b
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: e6c221299d80e6e66858db607c6b4696942b8a61

+ 9 - 11
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md

@@ -6,27 +6,25 @@ English | [中文](2026-08-10-product-subagent-providers-in-shared-host.zh.md)
 
 ## Problem
 
-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, and host consumers resolve the same registry across sessions. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
+The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) are independently installable packages loaded beside the common subagent tool. Agent Presets are 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, and host consumers resolve the same registry across sessions. Bundle installation and Preset tool grant are therefore separate deployment and agent-authoring decisions.
 
-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
 
-Every shipped Profile loads the fixed `codex` and `claude-code` providers once through the base bundle's host plane. 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 for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
+When installed in a Profile, each product Bundle loads its fixed `codex` or `claude-code` provider exactly once in the shared Host plane. 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 for `subagent_codex` and `subagent_claude_code`, so a preset can grant neither tool, either one, or both without changing the provider registry. A tool whose provider Bundle is not installed remains unavailable rather than mounting another provider in the Agent plane.
 
-This decision supersedes only the opt-in composition placement recorded by the provider-contract note. That 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 [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) partially supersedes only this note's former default-inclusion choice: the base bundle excludes both providers, and each provider package owns its directly installable Bundle patch. This note continues to own process-wide Host placement whenever a product Bundle 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, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
-
-The current base dependency closure still includes the Claude Agent SDK's optional platform CLI payload even though production resolves the host `claude`. Removing that unused payload belongs to the separate product installation-closure follow-up; this placement decision neither installs it dynamically nor treats it as the production executable.
+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. Bundle loading does not install a product, create product state, probe a version, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
 
 ## Verification
 
-The base Loader test proves both provider names register exactly once and no product process starts during Profile boot. Real Agent Preset composition covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
+Real composition loads the selected set of no product Bundle, Codex only, Claude Code only, or both, and crosses it with Agent Presets that grant none, either, or both tools. It proves the Host registry equals the installed Bundle set, model-visible tools equal the installed-and-granted intersection, and no product process starts during composition. Preset edit coverage retains generation isolation. Keyless ACP snapshots pin the model-visible tool schemas, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
 
 ## Alternatives considered
 
-**Keep product providers opt-in at the Profile layer.** This preserves a smaller default dependency closure, but a copied or agent-authored Preset row is not usable unless the person also discovers and edits a second composition layer. It leaves the general Preset entry incomplete for these otherwise ordinary tools.
+**Keep both dormant providers in every base Profile.** This makes every matching Preset row immediately usable, but forces every production installation to carry both provider packages and the Claude Agent SDK even when neither integration is wanted.
 
 **Store global or per-Profile product enable switches.** A process switch competes with the Preset as owner of model-visible tools and cannot express two sessions using different combinations. Availability and authentication are deployment facts, not another persisted product state.
 
@@ -36,6 +34,6 @@ The base Loader test proves both provider names register exactly once and no pro
 
 ## Consequences
 
-A user manages both products through the same Agent Preset authoring path as other plugins, and each new session receives exactly the tools its chosen preset contributes. Every Profile carries two dormant provider registrations, so unused products consume package and module-loading footprint but no product process, login, model call, or product home.
+A user installs only the product Bundles available to a Profile and manages model-visible grants through the same Agent Preset authoring path as other plugins. Each new session receives the intersection of its preset's tool rows and the Profile's installed providers. An installed but ungranted product remains dormant and consumes its package and module-loading footprint but no product process, login, model call, or product home; an uninstalled product contributes no provider or SDK closure.
 
-The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is the current Claude SDK optional-payload installation cost, which stays explicitly deferred rather than being hidden behind another enable state or installer lifecycle.
+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.

+ 9 - 11
.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md

@@ -6,27 +6,25 @@ Status: implemented
 
 ## 问题
 
-[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。如果要求用户同时编辑 Profile 和 Preset,也会使通用 preset 行本身不完整。
+[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)是可独立安装的包,由部署环境在通用 subagent 工具旁加载。Agent Preset 是单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。因此,Bundle 安装与 Preset 工具授权分别属于部署决策和 agent 创作决策。
 
-归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具是否启用仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
+归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具授权仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
 
 ## 决策
 
-每个随发行版交付的 Profile 都会通过 base 组合包的宿主平面,把固定的 `codex` 与 `claude-code` 提供方各加载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
+产品 Bundle 安装到 Profile 后,会在共享 Host 平面中恰好加载一次其固定的 `codex` 或 `claude-code` 提供方。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不授权任何工具、只授权其中一个或同时授权两者,而无需更改提供方注册表。若工具对应的提供方 Bundle 尚未安装,该工具仍不可用,而不会在 Agent 平面中另行挂载提供方。
 
-本决策仅取代提供方约定说明所记录的、原先由用户选择启用的组装位置。该说明仍负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
+[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只部分取代本说明先前关于默认包含提供方的选择:base 组合包排除两个提供方,每个提供方包负责其可直接安装的 Bundle patch。本说明继续负责产品 Bundle 安装后进程级的 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
 
-这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
-
-当前 base 依赖闭包仍包含 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷,尽管生产环境解析的是宿主提供的 `claude`。移除这份未使用载荷属于独立的产品安装闭包后续项;本归属决策既不会动态安装它,也不会将它当作生产可执行文件。
+这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Bundle 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
 
 ## 验证
 
-base Loader 测试证明两个提供方名称都恰好注册一次,而且 Profile 启动期间不会启动产品进程。真实 Agent Preset 组装覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。无密钥 ACP(Agent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
+真实组装会加载未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装这四种集合,并与不授权工具、仅授权其中一个或同时授权两者的 Agent Preset 完整交叉。测试证明 Host 注册表等于已安装 Bundle 集合,模型可见工具等于已安装且已授权集合的交集,并且组装期间不会启动产品进程。Preset 编辑覆盖继续证明代际隔离。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
 
 ## 考虑过的替代方案
 
-**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但复制或由 agent 创作的 Preset 行无法直接使用,除非用户还发现并编辑第二个组装层。对于这些本来与其他工具无异的工具,通用 Preset 入口仍不完整。
+**在每个 base Profile 中保留两个休眠提供方。** 这样每条匹配的 Preset 行都能立即使用,但即使用户不需要任一集成,每次生产安装仍会携带两个提供方包和 Claude Agent SDK。
 
 **存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
 
@@ -36,6 +34,6 @@ base Loader 测试证明两个提供方名称都恰好注册一次,而且 Prof
 
 ## 后果
 
-用户通过与其他插件相同的 Agent Preset 创作路径管理两个产品,每个新会话只会获得其所选 preset 所贡献的工具。每个 Profile 都携带两个休眠的提供方注册,因此未使用的产品会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录。
+用户只安装 Profile 可用的产品 Bundle,并通过与其他插件相同的 Agent Preset 创作路径管理模型可见授权。每个新会话会获得其 preset 工具行与 Profile 已安装提供方的交集。已安装但未授权的产品保持休眠,会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录;未安装的产品不会进入提供方或 SDK 依赖闭包。
 
-宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是当前 Claude SDK 可选载荷的安装成本继续被明确延期处理,而不会隐藏在另一种启用状态或安装程序生命周期之后。
+Host 注册表仍是提供方的唯一权威,每个 Bundle 仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。

+ 2 - 2
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
-2026-08-04-claude-code-and-codex-subagent-backends.md: ccc96d6c998c4ab958a7eea1e502d036d16ec90d
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 740eeb633e336d5b01cb0b84fb656612e690959d
+2026-08-04-claude-code-and-codex-subagent-backends.md: 80dc7ad8488b3ed557361ab3e88948e56763c860
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: c6b5f890677ea85f68f5e1b388b1294cea6659a7

+ 3 - 3
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md

@@ -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: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) supersedes the original opt-in composition placement. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection and background execution are not model arguments.
+The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) owns process-wide placement when a provider is installed, while the [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their optional direct Bundle installation and exclusion from the default distribution. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection and background execution are not model arguments.
 
 Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
 
@@ -65,7 +65,7 @@ The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its rea
 
 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, inherited temporary host-setting marker, 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 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, inherited temporary host-setting marker, 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. Loader and optional Bundle-composition evidence resolve the selected 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 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.
 
@@ -87,7 +87,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
 
 ## Consequences
 
-Users delegate through two stable foreground tools backed by the official product integrations. Their Profile placement and per-Preset exposure are owned by the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md); this note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
+Users delegate through two stable foreground tools backed by the official product integrations. Installed providers remain in the process-wide Host and tools remain per Preset under the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md); optional package availability and default exclusion are owned by the [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
 
 Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. 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.
 

+ 3 - 3
.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[共享 profile 宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)取代原先由用户选择启用的组装位置。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
+harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[共享 profile 宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责提供方安装后的进程级放置,[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责其可选直接 Bundle 安装与默认发行排除。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
 
 这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具会禁用后台执行,并使用 `maxDepth: 'provider-managed'`,将递归策略留给进程外产品,而不是发送提供方无法强制执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。
 
@@ -65,7 +65,7 @@ Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实
 
 带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
 
-Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
+Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Claude Code 2.1.220 CLI 作为确定性兼容性 fixture(测试前置数据),且该 fixture 经生产环境所用的同一原生可执行文件解析路径运行。其真实产品测试会观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与可选 Bundle 组装证据会按名称解析已选择的产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
 
 带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
 
@@ -87,7 +87,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
 
 ## 后果
 
-用户通过官方产品集成支持的两个稳定前台工具进行委派。它们在 Profile 中的归属和按 Preset 暴露方式由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
+用户通过官方产品集成支持的两个稳定前台工具进行委派。已安装提供方位于进程级 Host、工具按 Preset 暴露,这些规则由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;可选包可用性与默认排除由[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
 
 每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
 

+ 2 - 2
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/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: 94cfe82d0aa42076f3c0723ed99a83a1e53e3724
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a9dbdcff748ea46dc84c1480e6e50a945219a5c7

+ 7 - 3
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md

@@ -10,16 +10,20 @@ English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers
 
 ## 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 existing provider package is instead a directly installable Profile Bundle whose `dsh.bundle.patch` points to one package-owned `cordis.patch.yml`. That patch contributes exactly one self-provider Host row and no Agent tool row.
+
+The two Bundles remain independent. The Codex Bundle owns its `@deepseek-ai/dsh-sdk-protocol` runtime dependency; the Claude Code Bundle owns its Agent SDK runtime dependency. Installing one does not pull in the other, and the default `@deepseek-ai/dsh` production closure contains neither provider nor the Claude Agent SDK. An 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, authenticate, configure, or grant model access to either product.
 
 ## 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 each Bundle manifest, exported patch, exact self-provider row, and product-specific runtime dependency. Workspace validation discovers Bundle manifests by declaration rather than directory. Production-closure tests prove the default, Codex-only, and Claude-only dependency boundaries, while real Bundle-patch and Agent-Preset composition covers all four installed sets against all four tool sets and proves composition starts no product process. The base bundle test continues to reject both provider dependencies and configuration rows.
 
 ## 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 package, or both, directly; the changed Host availability takes effect on the next Profile start. A separately authored Agent Preset still grants the model-visible tool only to newly composed Sessions. No wrapper package, meta Bundle, dynamic installer, or persisted product-enable state is introduced.

+ 7 - 3
.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md

@@ -10,16 +10,20 @@ Status: implemented
 
 ## 决策
 
-本决策取代[共享 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 自己负责运行时依赖 `@deepseek-ai/dsh-sdk-protocol`;Claude Code Bundle 自己负责 Agent SDK 运行时依赖。安装其中一个不会带入另一个,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含 Claude Agent SDK。已安装的 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装不会启动产品、验证身份、配置产品或向模型授予任一产品的访问权。
 
 ## 验证
 
-base 组合包测试会拒绝这两个提供方依赖与配置行。Cordis 配置验证要求显式 examples 声明其引用的提供方包。
+包测试会固定每个 Bundle 的 manifest、导出的 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会按 Bundle 声明发现 manifest,而非按目录发现。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合与四种工具集合的完整矩阵,并证明组装不会启动产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
 
 ## 考虑过的替代方案
 
 **在 base 组合包中保留休眠提供方。** 休眠提供方不会启动产品进程,但其包仍会进入每次生产 NPM 安装。
 
+**新增 wrapper 或 meta Bundle。** 第三个包会重复安装责任,使独立移除变得更间接,却不会贡献新的运行时能力。
+
 ## 后果
 
-安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。使用任一集成都需要显式 Profile 配置。
+安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除任一提供方包,也可以同时操作两者;Host 可用性的变化会在下次 Profile 启动时生效。单独创作的 Agent Preset 仍只会向新组装的 Session 授予模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。

+ 2 - 0
apps/cli/config/agent-presets/code/agent.cordis.yml

@@ -201,6 +201,8 @@
     # Product providers are host-plane singletons. Copy this preset, then
     # remove `disabled` from either ordinary tool row to expose that product
     # only to agents composed from the copy.
+    # Install the matching optional Provider Bundle in this Profile and restart
+    # the Host before enabling either template. Installation alone grants no tool.
     - id: tool-subagent-codex
       name: '@deepseek-ai/dsh-tool-subagent'
       disabled: true

+ 2 - 0
apps/cli/config/agent-presets/cordis/agent.cordis.yml

@@ -188,6 +188,8 @@
     # Product providers are host-plane singletons. Copy this preset, then
     # remove `disabled` from either ordinary tool row to expose that product
     # only to agents composed from the copy.
+    # Install the matching optional Provider Bundle in this Profile and restart
+    # the Host before enabling either template. Installation alone grants no tool.
     - id: tool-subagent-codex
       name: '@deepseek-ai/dsh-tool-subagent'
       disabled: true

+ 9 - 2
apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md

@@ -123,7 +123,14 @@ After a clean mount-validation, ask the user to start a session on the new prese
 
 ## Native product subagents
 
-Codex and Claude Code providers already live in the host composition. A preset chooses either product by contributing the same ordinary delegation-tool row used for spawn and fork; never move a product provider into the preset and never add a product-specific settings field.
+Codex and Claude Code providers are optional Profile Bundles. Install only the products the 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
+```
+
+The Bundle owns 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 either package with `dsh plugin --profile <name> remove <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:
 
@@ -147,7 +154,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o
     maxDepth: provider-managed
 ```
 
-The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must 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 an installed matching provider, and enabling both exposes the installed intersection. The host must already provide `codex` or `claude` on `PATH`; neither the Bundle nor the preset installs, authenticates, selects a model for, starts, or probes either product.
 
 ## What not to move into a preset
 

+ 2 - 0
apps/cli/config/agent-presets/standard/agent.cordis.yml

@@ -200,6 +200,8 @@
     # Product providers are host-plane singletons. Copy this preset, then
     # remove `disabled` from either ordinary tool row to expose that product
     # only to agents composed from the copy.
+    # Install the matching optional Provider Bundle in this Profile and restart
+    # the Host before enabling either template. Installation alone grants no tool.
     - id: tool-subagent-codex
       name: '@deepseek-ai/dsh-tool-subagent'
       disabled: true

+ 2 - 2
apps/cli/reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/cli/reference/README.md
-README.md: 17d63fe73ea2b3bda74e9c4da91555b34c21f4ab
-README.zh.md: ee99592fbba07b52b29db534e12c1d0ef23e5ef0
+README.md: ed6a645c8587044f5dfd3f222d9700058380196b
+README.zh.md: a75ef3426fc3794c0968d08d0cb8603347edfa06

+ 12 - 0
apps/cli/reference/README.md

@@ -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` retain their existing hot-reload behavior. On the next start, each installed product Bundle registers only its dormant Host provider and does not start, install, authenticate, or configure the native product. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the matching row before a new Agent can see that tool. Installing one provider never installs the other product package; the default dsh dependency closure includes neither provider nor the Claude Agent SDK.
+
 ```sh
 dsh plugin --profile tui add github:deepseek-harness/turtle-ui
 dsh plugin --profile tui remove turtle-ui

+ 12 - 0
apps/cli/reference/README.zh.md

@@ -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,不会启动、安装、认证或配置原生产品。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK。
+
 ```sh
 dsh plugin --profile tui add github:deepseek-harness/turtle-ui
 dsh plugin --profile tui remove turtle-ui

+ 15 - 0
apps/cli/tests/built-bin.e2e.ts

@@ -641,6 +641,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 })

+ 85 - 43
apps/cli/tests/web-agent-presets.e2e.ts

@@ -1,5 +1,5 @@
 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'
@@ -9,7 +9,7 @@ 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,6 +26,8 @@ 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_PATCH = join(REPO_ROOT, 'packages/subagent/subagent-codex/cordis.patch.yml')
+const CLAUDE_CODE_PATCH = join(REPO_ROOT, 'packages/subagent/subagent-claude-code/cordis.patch.yml')
 /** The installation anchor whose dependency surface the preset module fallback mirrors. */
 const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
 const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
@@ -43,7 +45,11 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
  * touch the network, or write outside the test. Everything that decides an
  * agent's capabilities is the real thing, including both shipped presets.
  */
-async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promise<Context> {
+async function bootWeb(
+  settingsFile: string,
+  extra: PatchOptions[] = [],
+  profilePackages: readonly string[] = [],
+): Promise<Context> {
   const storageRoot = join(dirname(settingsFile), 'storages')
   const patches: PatchOptions[] = [
     ...loadOverlayPatches('dsh-test', BASE_PATCH),
@@ -112,6 +118,16 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
   healProfilesModuleFallback(INSTALL_ANCHOR, home)
   const profileDir = join(home, 'profiles', 'spec')
   await mkdir(profileDir, { recursive: true })
+  // Product Bundles are installed into the Profile, not the dsh app. Model
+  // 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')
+  }
   const rootConfig = join(profileDir, 'cordis.yml')
   await writeFile(rootConfig, '[]\n')
   return await boot('dsh-test', rootConfig, patches, (bootCtx) => {
@@ -416,17 +432,17 @@ 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 subagent Bundle and user-preset intersection', () => {
+  const presetIds = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
+  type Product = 'codex' | 'claude-code'
 
-  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')
@@ -438,50 +454,75 @@ describe('product subagent rows in user presets', () => {
       await mkdir(directory, { recursive: true })
       await writeFile(join(directory, 'agent.cordis.yml'), composition)
     }
-    productCtx = await bootWeb(settingsFile, [{
-      id: 'agent-presets',
-      config: {
-        default: 'standard',
-        roots: [
-          { path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
-          { path: userRoot, trust: 'user' },
-        ],
-        includeUserRoot: false,
+    const productPatches = installed.flatMap(product => loadOverlayPatches(
+      'dsh-test',
+      product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH,
+    ))
+    return await bootWeb(settingsFile, [
+      ...productPatches,
+      {
+        id: 'agent-presets',
+        config: {
+          default: 'standard',
+          roots: [
+            { path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
+            { path: userRoot, trust: 'user' },
+          ],
+          includeUserRoot: false,
+        },
       },
-    }])
-  }, 120_000)
-
-  afterAll(async () => {
-    await productCtx.fiber.dispose()
-  })
+    ], installed.map(product => dirname(product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH)))
+  }
 
-  it('composes none, either product, or both without changing the shared host registry', async () => {
-    const expected = new Map<string, string[]>([
+  it('composes the intersection of installed Bundles and enabled preset rows', async () => {
+    const enabledByPreset = new Map<string, Product[]>([
       ['products-none', []],
-      ['products-codex', ['subagent_codex']],
-      ['products-claude', ['subagent_claude_code']],
-      ['products-both', ['subagent_claude_code', 'subagent_codex']],
+      ['products-codex', ['codex']],
+      ['products-claude', ['claude-code']],
+      ['products-both', ['codex', 'claude-code']],
     ])
-    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),
-      })
+    const installations: Product[][] = [
+      [],
+      ['codex'],
+      ['claude-code'],
+      ['codex', 'claude-code'],
+    ]
+
+    for (const installed of installations) {
+      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(productCtx.subagents.list()
+          .filter(name => name === 'codex' || name === 'claude-code')
+          .sort())
+          .toEqual([...installed].sort())
+        for (const [id, enabled] of enabledByPreset) {
+          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 expectedTools = enabled
+              .filter(product => installed.includes(product))
+              .map(product => product === 'codex' ? 'subagent_codex' : 'subagent_claude_code')
+              .sort()
+            expect(toolNames(productCtx, handle.agent)
+              .filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
+              .toEqual(expectedTools)
+          } 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({
@@ -505,8 +546,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', () => {

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 56e029df192f28a787748b12074ee4dfe67d1c58
-module-graph.zh.md: 839c5edf758a3076874643cb6bdbd91954ca369e
+module-graph.md: 4518321070889ffd6206be727b15b4e4c6d542d0
+module-graph.zh.md: 3516346d8c458e196f32864837f9ee323f55a304

+ 7 - 8
docs/module-graph.md

@@ -990,6 +990,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_inprocess --> pkg_agent
   pkg_subagent_inprocess --> pkg_invariants
   pkg_subagent_inprocess --> pkg_llm
@@ -1072,13 +1078,6 @@ flowchart TD
   pkg_compact_basic --> pkg_llm
   pkg_compact_basic --> pkg_session
   pkg_compact_basic --> pkg_token_meter
-  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 --> pkg_agent
   pkg_subagent_fork --> pkg_invariants
   pkg_subagent_fork --> pkg_session
@@ -1526,6 +1525,7 @@ flowchart TD
 | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/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/support/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/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/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/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1539,7 +1539,6 @@ flowchart TD
 | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/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`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |

+ 7 - 8
docs/module-graph.zh.md

@@ -992,6 +992,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_inprocess --> pkg_agent
   pkg_subagent_inprocess --> pkg_invariants
   pkg_subagent_inprocess --> pkg_llm
@@ -1074,13 +1080,6 @@ flowchart TD
   pkg_compact_basic --> pkg_llm
   pkg_compact_basic --> pkg_session
   pkg_compact_basic --> pkg_token_meter
-  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 --> pkg_agent
   pkg_subagent_fork --> pkg_invariants
   pkg_subagent_fork --> pkg_session
@@ -1528,6 +1527,7 @@ flowchart TD
 | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/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/support/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/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/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/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1541,7 +1541,6 @@ flowchart TD
 | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/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`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
 | [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |

+ 2 - 16
examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml

@@ -1,5 +1,5 @@
-# Test-only composition of both public opt-in providers and foreground tools.
-# The owning e2e boots this tree but never invokes a model or product process.
+# Test-only composition of the Claude Code foreground tool around its Bundle-supplied provider.
+# The owning e2e applies the package's real patch and never invokes a model or product process.
 - id: fixture
   name: './fixture.ts'
 
@@ -9,20 +9,6 @@
 - id: subprocess
   name: '@deepseek-ai/dsh-subprocess-local'
 
-- id: subagent-codex
-  name: '@deepseek-ai/dsh-subagent-codex'
-
-- id: subagent-claude-code
-  name: '@deepseek-ai/dsh-subagent-claude-code'
-
-- id: tool-subagent-codex
-  name: '@deepseek-ai/dsh-tool-subagent'
-  config:
-    provider: codex
-    toolName: subagent_codex
-    enableRunInBackground: false
-    maxDepth: 'provider-managed'
-
 - id: tool-subagent-claude-code
   name: '@deepseek-ai/dsh-tool-subagent'
   config:

+ 22 - 35
examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts

@@ -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,41 +24,27 @@ const ctx = await boot(
 )
 
 try {
-  const providerNames = ['codex', 'claude-code'] as const
-  const toolNames = ['subagent_codex', 'subagent_claude_code'] as const
-  const providers = providerNames.map((providerName) => {
-    const provider = ctx.subagents.getProvider(providerName)
-    if (provider === undefined) {
-      throw new Error(`${providerName} provider was not registered`)
-    }
-    return {
+  const provider = ctx.subagents.getProvider('claude-code')
+  if (provider === undefined) throw new Error('claude-code provider was not registered')
+  const tool = ctx.tools.schemas().find(schema => schema.name === 'subagent_claude_code')
+  if (tool === undefined) throw new Error('subagent_claude_code tool was not registered')
+  const properties = tool.parameters.properties
+  if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) {
+    throw new Error('subagent_claude_code has invalid parameter properties')
+  }
+
+  process.stdout.write(`${JSON.stringify({
+    providers: ctx.subagents.list(),
+    provider: {
       name: provider.name,
       capabilities: provider.capabilities,
       inheritsParentContext: provider.inheritsParentContext,
-    }
-  })
-  const tools = toolNames.map((toolName) => {
-    const tool = ctx.tools.schemas().find(schema => schema.name === toolName)
-    if (tool === undefined) throw new Error(`${toolName} tool was not registered`)
-    const properties = tool.parameters.properties
-    if (
-      typeof properties !== 'object'
-      || properties === null
-      || Array.isArray(properties)
-    ) {
-      throw new Error(`${toolName} has invalid parameter properties`)
-    }
-    return {
+    },
+    tool: {
       name: tool.name,
       parameterNames: Object.keys(properties).sort(),
       required: tool.parameters.required,
-    }
-  })
-
-  process.stdout.write(`${JSON.stringify({
-    registeredProviders: ctx.subagents.list(),
-    providers,
-    tools,
+    },
     starts,
   })}\n`)
 } finally {

+ 2 - 5
examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml

@@ -1,5 +1,5 @@
-# Test-only composition of the public opt-in provider and foreground tool.
-# The owning e2e boots this tree but never invokes the model or Codex.
+# Test-only composition of the foreground tool around a Bundle-supplied provider.
+# The owning e2e applies the package's real patch and never invokes the model or Codex.
 - id: fixture
   name: './fixture.ts'
 
@@ -9,9 +9,6 @@
 - id: subprocess
   name: '@deepseek-ai/dsh-subprocess-local'
 
-- id: subagent-codex
-  name: '@deepseek-ai/dsh-subagent-codex'
-
 - id: tool-subagent-codex
   name: '@deepseek-ai/dsh-tool-subagent'
   config:

+ 5 - 4
examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts

@@ -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

+ 2 - 2
packages/bundle/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/README.md
-README.md: 696aa9ef7bbed23774f2b9ab2648ca83edcf0978
-README.zh.md: 2bb22c7949d759f404288548ea0eccfa0aac866b
+README.md: 4d7a064939ae04f25737b324ec35332b7b944f80
+README.zh.md: 8910b33a97acd2ef3ee5b659305739246004de01

+ 2 - 0
packages/bundle/README.md

@@ -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) |

+ 2 - 0
packages/bundle/README.zh.md

@@ -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 - 2
packages/bundle/base/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/bundle/base/README.md
-README.md: bd38f39f58ee1f765ff34d40cf57cc6daed2b32b
-README.zh.md: 2c6ff8513bae2b7d4b595733e83223bb7af34780
+README.md: a963bcca671c613ebdcc7b453384b1d9b8393662
+README.zh.md: d6594dd35c6932da38f6c8d0069623037dff24db

+ 1 - 2
packages/bundle/base/README.md

@@ -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. Codex and Claude Code providers load dormant; Agent Presets independently decide whether their agent contributes either model-facing delegation tool. 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 package](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider nor the Claude Agent SDK. 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.
 
@@ -19,5 +19,4 @@ None directly; each inserted row's package owns its effect.
 ## Known Limitations and Deferred Work
 
 - **A patch replaces whole row configs** — profile overrides must restate every field a row keeps; there is no deep-merge layer.
-- **Claude's SDK platform CLI remains in the Profile install closure** — the base bundle depends on the Claude provider, whose production path resolves the host `claude`; removing the SDK's unused optional payload is deferred to the product installation-closure follow-up.
 - **The Windows temp grant is a private per-session subdirectory** — `workspace-write` confines writes to the workspace plus the session's own temp subdirectory (`<temp>\dsh-<hash>`, TMP/TEMP rewritten for confined children); `read-only` grants nothing. See `@deepseek-ai/dsh-sandbox-windows-acl`.

+ 1 - 2
packages/bundle/base/README.zh.md

@@ -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` 列表中的第一层。Codex 与 Claude Code provider 以休眠状态加载;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 包](../../subagent/README.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider,也不包含 Claude Agent SDK。后续的组合包层(例如 [`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` 服务,配方不完整会在加载时 fail loud)。POSIX 主机看到的是被禁用的 pwsh 行。
 
@@ -19,5 +19,4 @@ patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 
 ## 已知限制与延期工作
 
 - **patch 会替换整行 `config`**:profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。
-- **Claude SDK 的平台 CLI(命令行界面)仍在 Profile 安装闭包中**:base 组合包依赖 Claude 提供方,其生产路径解析宿主提供的 `claude`;移除 SDK 中未使用的可选载荷,推迟到产品安装闭包后续项处理。
 - **Windows 的临时目录授权是按会话的私有子目录**——`workspace-write` 把写入限制在工作区与会话自己的 temp 子目录(`<temp>\dsh-<hash>`,受限子进程的 TMP/TEMP 被改写);`read-only` 不授予任何写入。见 `@deepseek-ai/dsh-sandbox-windows-acl`。

+ 2 - 2
packages/subagent/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subagent/README.md
-README.md: 134028c9993255464c08d912d305c65ed85d65c0
-README.zh.md: ab4284d0525ceed349a778385c918a9dec19406f
+README.md: 870cb4be0dc9dc11fe43e6f1e03281a528a769aa
+README.zh.md: 122c34c264d01974a18e9ec9fe5cb661bd1e2b67

+ 2 - 0
packages/subagent/README.md

@@ -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 also independent 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 installed package registers only its own dormant Host provider. Full Agent Presets keep separate disabled tool templates, so installation alone exposes no model tool. Removing one package withdraws only that provider 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.

+ 2 - 0
packages/subagent/README.zh.md

@@ -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 仍保留彼此独立且默认禁用的工具模板,因此只安装 Bundle 不会向模型暴露工具。移除其中一个包后,下一次 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 - 2
packages/subagent/subagent-claude-code/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
-README.md: 17b14e847baea3eadda7129b5e49f5e65b668cc8
-README.zh.md: 2f59144d5bd9f26a58773e6dd53909b2b0e8da14
+README.md: b1a5c4bb4b9d1c3221c38092d8b0b967888b7746
+README.zh.md: 0b5419c3b0c57798068556891b2940e3689a7f74

+ 13 - 2
packages/subagent/subagent-claude-code/README.md

@@ -31,15 +31,26 @@ The provider advertises no optional start-time capabilities and reports `inherit
 
 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.
 
-Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. A custom host composition can still use both rows directly.
+This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; its declared `cordis.patch.yml` layer registers only the dormant `claude-code` Host provider and starts no Claude process. Removing the package withdraws that provider 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. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to new agents composed from the copy. The Profile's own patch can replace the Bundle row's complete `config`, while a custom Host composition can still mount the package directly.
 
 ```yaml
+# $DSH_HOME/profiles/<name>/cordis.patch.yml (optional provider override)
 - id: subagent-claude-code
-  name: '@deepseek-ai/dsh-subagent-claude-code'
   config:
     env:
       ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
+```
 
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
 - id: tool-subagent-claude-code
   name: '@deepseek-ai/dsh-tool-subagent'
   disabled: true

+ 13 - 2
packages/subagent/subagent-claude-code/README.zh.md

@@ -31,15 +31,26 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 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` 等普通环境变量仍会被继承。
 
-随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。自定义宿主组装仍可直接使用两条配置行。
+本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 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 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_claude_code`。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
 
 ```yaml
+# $DSH_HOME/profiles/<name>/cordis.patch.yml (optional provider override)
 - id: subagent-claude-code
-  name: '@deepseek-ai/dsh-subagent-claude-code'
   config:
     env:
       ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
+```
 
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
 - id: tool-subagent-claude-code
   name: '@deepseek-ai/dsh-tool-subagent'
   disabled: true

+ 6 - 0
packages/subagent/subagent-claude-code/cordis.patch.yml

@@ -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'

+ 7 - 0
packages/subagent/subagent-claude-code/package.json

@@ -22,15 +22,22 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
+    "./cordis.patch.yml": "./cordis.patch.yml",
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
   "files": [
     "lib/index.js",
     "lib/invariant.js",
+    "cordis.patch.yml",
     "lib/types/**/*.d.ts"
   ],
   "license": "BSD-3-Clause",
+  "dsh": {
+    "bundle": {
+      "patch": "./cordis.patch.yml"
+    }
+  },
   "peerDependencies": {
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",

+ 27 - 37
packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts

@@ -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,60 +13,49 @@ 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 both opt-in packages and foreground tools without starting either product', async () => {
+describe('Claude Code provider public Loader composition', () => {
+  it('loads its Bundle patch and foreground tool without starting Claude Code', 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-code'],
-      providers: [
-        {
-          name: 'codex',
-          capabilities: {
-            outputSchema: false,
-            depthLimit: false,
-            toolFilter: false,
-            persona: false,
-          },
-          inheritsParentContext: false,
+      providers: ['claude-code'],
+      provider: {
+        name: 'claude-code',
+        capabilities: {
+          outputSchema: false,
+          depthLimit: false,
+          toolFilter: false,
+          persona: false,
         },
-        {
-          name: 'claude-code',
-          capabilities: {
-            outputSchema: false,
-            depthLimit: false,
-            toolFilter: false,
-            persona: false,
-          },
-          inheritsParentContext: false,
-        },
-      ],
-      tools: [
-        {
-          name: 'subagent_codex',
-          parameterNames: ['description', 'prompt'],
-          required: ['description', 'prompt'],
-        },
-        {
-          name: 'subagent_claude_code',
-          parameterNames: ['description', 'prompt'],
-          required: ['description', 'prompt'],
-        },
-      ],
+        inheritsParentContext: false,
+      },
+      tool: {
+        name: 'subagent_claude_code',
+        parameterNames: ['description', 'prompt'],
+        required: ['description', 'prompt'],
+      },
       starts: 0,
     })
   }, LOADER_SMOKE_TEST_TIMEOUT_MS)

+ 29 - 0
packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts

@@ -1,4 +1,7 @@
+import { readFileSync } from 'node:fs'
+import { resolve } from 'node:path'
 import { PassThrough } from 'node:stream'
+import { fileURLToPath } from 'node:url'
 import type {
   Options,
   Query,
@@ -8,6 +11,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,
@@ -282,6 +286,31 @@ 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>
+      exports?: Record<string, unknown>
+      files?: string[]
+      dsh?: { bundle?: { patch?: string } }
+    }
+    expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
+    expect(manifest.exports?.['./cordis.patch.yml']).toBe('./cordis.patch.yml')
+    expect(manifest.files).toContain('cordis.patch.yml')
+    expect(manifest.dependencies).toHaveProperty('@anthropic-ai/claude-agent-sdk')
+    expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
+
+    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' },

+ 2 - 2
packages/subagent/subagent-codex/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
-README.md: 3d59ca1eaf3db9dd9d9d2cd451692ebd2a956ef4
-README.zh.md: b60cb1bba9b2d7b3f61c544c1600862a0ad6ce5b
+README.md: 18e805f0e0a1d8d33ba77182ed73d213beb62e07
+README.zh.md: ff22fe151021dbff496165225ed4e8724af4d18c

+ 13 - 2
packages/subagent/subagent-codex/README.md

@@ -27,15 +27,26 @@ The provider advertises no optional start-time capabilities and reports `inherit
 
 Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. 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.
 
-Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. A custom host composition can still use both rows directly.
+This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; its declared `cordis.patch.yml` layer registers only the dormant `codex` Host provider and starts no Codex process. Removing the package withdraws that provider 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. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to new agents composed from the copy. The Profile's own patch can replace the Bundle row's complete `config`, while a custom Host composition can still mount the package directly.
 
 ```yaml
+# $DSH_HOME/profiles/<name>/cordis.patch.yml (optional provider override)
 - id: subagent-codex
-  name: '@deepseek-ai/dsh-subagent-codex'
   config:
     env:
       OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
+```
 
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
 - id: tool-subagent-codex
   name: '@deepseek-ai/dsh-tool-subagent'
   disabled: true

+ 13 - 2
packages/subagent/subagent-codex/README.zh.md

@@ -27,15 +27,26 @@
 
 生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
 
-随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。自定义宿主组装仍可直接使用两条配置行。
+本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 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 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_codex`。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
 
 ```yaml
+# $DSH_HOME/profiles/<name>/cordis.patch.yml (optional provider override)
 - id: subagent-codex
-  name: '@deepseek-ai/dsh-subagent-codex'
   config:
     env:
       OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
+```
 
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
 - id: tool-subagent-codex
   name: '@deepseek-ai/dsh-tool-subagent'
   disabled: true

+ 6 - 0
packages/subagent/subagent-codex/cordis.patch.yml

@@ -0,0 +1,6 @@
+# This optional Profile layer registers the dormant Codex provider. Agent
+# presets separately decide whether one session receives its delegation tool.
+
+- insert:
+    - id: subagent-codex
+      name: '@deepseek-ai/dsh-subagent-codex'

+ 8 - 2
packages/subagent/subagent-codex/package.json

@@ -22,19 +22,25 @@
       "types": "./lib/types/invariant.d.ts",
       "default": "./lib/invariant.js"
     },
+    "./cordis.patch.yml": "./cordis.patch.yml",
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
   "files": [
     "lib/index.js",
     "lib/invariant.js",
+    "cordis.patch.yml",
     "lib/types/**/*.d.ts"
   ],
   "license": "BSD-3-Clause",
+  "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,6 +48,7 @@
     "@deepseek-ai/cordis": "workspace:^"
   },
   "dependencies": {
+    "@deepseek-ai/dsh-sdk-protocol": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "devDependencies": {
@@ -50,7 +57,6 @@
     "@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:^",

+ 9 - 0
packages/subagent/subagent-codex/tests/loader-composition.e2e.ts

@@ -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,6 +13,13 @@ 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', () => {
@@ -22,6 +30,7 @@ describe('Codex provider public Loader composition', () => {
       binScript: driver,
       libBinScript: driver,
       configPath,
+      binArgs: [configPath, bundlePatchPath],
       tsconfigPath: repoTsconfig,
       env: {
         // Loading the optional package must not probe or start a Codex binary.

+ 31 - 0
packages/subagent/subagent-codex/tests/subagent-codex.spec.ts

@@ -1,6 +1,10 @@
+import { readFileSync } from 'node:fs'
+import { 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'
@@ -260,6 +264,33 @@ function turnCompleted(
 }
 
 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>
+      peerDependencies?: Record<string, string>
+      exports?: Record<string, unknown>
+      files?: string[]
+      dsh?: { bundle?: { patch?: string } }
+    }
+    expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
+    expect(manifest.exports?.['./cordis.patch.yml']).toBe('./cordis.patch.yml')
+    expect(manifest.files).toContain('cordis.patch.yml')
+    expect(manifest.dependencies).toHaveProperty('@deepseek-ai/dsh-sdk-protocol')
+    expect(manifest.peerDependencies).not.toHaveProperty('@deepseek-ai/dsh-sdk-protocol')
+    expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-claude-code')
+
+    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('resolves the fixed app-server command through the Windows npm shim boundary', () => {
     expect(codexAppServerArgv('win32')).toEqual([
       'cmd.exe',

+ 3 - 3
pnpm-lock.yaml

@@ -6939,6 +6939,9 @@ 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
@@ -6961,9 +6964,6 @@ importers:
       '@deepseek-ai/dsh-loader-smoke':
         specifier: workspace:^
         version: link:../../support/loader-smoke
-      '@deepseek-ai/dsh-sdk-protocol':
-        specifier: workspace:^
-        version: link:../../sdk/protocol
       '@deepseek-ai/dsh-session':
         specifier: workspace:^
         version: link:../../core/session

+ 2 - 0
scripts/check-workspace-constraints.ts

@@ -135,6 +135,8 @@ const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
   '@deepseek-ai/dsh-base': ['cordis.patch.yml'],
   '@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
   '@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
+  '@deepseek-ai/dsh-subagent-codex': ['cordis.patch.yml'],
+  '@deepseek-ai/dsh-subagent-claude-code': ['cordis.patch.yml'],
   '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
   // The Python runtime uses a distinct closed-resolution bin; the public CLI
   // keeps config-owned bare-package resolution through lib/bin.js.

+ 2 - 2
scripts/verify-config-source-ownership.spec.ts

@@ -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-codex')
     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-codex/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.',
     ])

+ 2 - 1
scripts/verify-config-source-ownership.ts

@@ -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',
 ]

+ 97 - 1
scripts/verify-cordis-config.spec.ts

@@ -4,8 +4,46 @@
  * metadata field must stay static, and a disabled expression must parse.
  */
 
+import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
 import { describe, expect, it } from 'vitest'
-import { metadataExpressionErrors } from './verify-cordis-config.ts'
+import {
+  bundleManifestPaths,
+  bundlePluginDependencyErrors,
+  metadataExpressionErrors,
+} from './verify-cordis-config.ts'
+
+interface WorkspaceManifest {
+  name?: string
+  dependencies?: Record<string, string>
+  optionalDependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+}
+
+const repoRoot = fileURLToPath(new URL('..', import.meta.url))
+
+function productionClosure(entry: string): Set<string> {
+  const manifests = new Map<string, WorkspaceManifest>()
+  for (const path of globSync(['apps/*/package.json', 'packages/*/*/package.json'], { cwd: repoRoot })) {
+    const manifest = JSON.parse(readFileSync(join(repoRoot, path), 'utf8')) as WorkspaceManifest
+    if (manifest.name !== undefined) manifests.set(manifest.name, manifest)
+  }
+  const visited = new Set<string>()
+  const pending = [entry]
+  for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
+    if (visited.has(name)) continue
+    visited.add(name)
+    const manifest = manifests.get(name)
+    pending.push(
+      ...Object.keys(manifest?.dependencies ?? {}),
+      ...Object.keys(manifest?.optionalDependencies ?? {}),
+      ...Object.keys(manifest?.peerDependencies ?? {}),
+    )
+  }
+  return visited
+}
 
 describe('verify-cordis-config metadata expressions', () => {
   it('accepts a disabled !!js expression', () => {
@@ -37,3 +75,61 @@ 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`,
+    ])
+  })
+
+  it('keeps the default and two optional product closures independent', () => {
+    const shipped = productionClosure('@deepseek-ai/dsh')
+    expect(shipped).not.toContain('@deepseek-ai/dsh-subagent-codex')
+    expect(shipped).not.toContain('@deepseek-ai/dsh-subagent-claude-code')
+    expect(shipped).not.toContain('@anthropic-ai/claude-agent-sdk')
+
+    const codex = productionClosure('@deepseek-ai/dsh-subagent-codex')
+    expect(codex).toContain('@deepseek-ai/dsh-sdk-protocol')
+    expect(codex).not.toContain('@deepseek-ai/dsh-subagent-claude-code')
+    expect(codex).not.toContain('@anthropic-ai/claude-agent-sdk')
+
+    const claudeCode = productionClosure('@deepseek-ai/dsh-subagent-claude-code')
+    expect(claudeCode).toContain('@anthropic-ai/claude-agent-sdk')
+    expect(claudeCode).not.toContain('@deepseek-ai/dsh-subagent-codex')
+  })
+})

+ 47 - 13
scripts/verify-cordis-config.ts

@@ -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> {