From 08a2c234c0b3f336d222e1575c1414faa06d4e99 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:46:12 +0800
Subject: [PATCH 001/134] refactor: remove codex/claude dep from direct builtin
dep
---
examples/package.json | 2 --
packages/bundle/base/package.json | 2 --
pnpm-lock.yaml | 12 ------------
3 files changed, 16 deletions(-)
diff --git a/examples/package.json b/examples/package.json
index 29818133ef..761dd0851f 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -72,8 +72,6 @@
"@deepseek-ai/dsh-spill-policy": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
- "@deepseek-ai/dsh-subagent-claude-code": "workspace:*",
- "@deepseek-ai/dsh-subagent-codex": "workspace:*",
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json
index e4f9167fd1..80859a038b 100644
--- a/packages/bundle/base/package.json
+++ b/packages/bundle/base/package.json
@@ -85,8 +85,6 @@
"@deepseek-ai/dsh-spill-local": "workspace:^",
"@deepseek-ai/dsh-spill-policy": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
- "@deepseek-ai/dsh-subagent-claude-code": "workspace:^",
- "@deepseek-ai/dsh-subagent-codex": "workspace:^",
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ab7b3dae65..306cd99243 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -608,12 +608,6 @@ importers:
'@deepseek-ai/dsh-subagent-acp':
specifier: workspace:*
version: link:../packages/subagent/subagent-acp
- '@deepseek-ai/dsh-subagent-claude-code':
- specifier: workspace:*
- version: link:../packages/subagent/subagent-claude-code
- '@deepseek-ai/dsh-subagent-codex':
- specifier: workspace:*
- version: link:../packages/subagent/subagent-codex
'@deepseek-ai/dsh-subagent-dsh-sdk':
specifier: workspace:*
version: link:../packages/subagent/subagent-dsh-sdk
@@ -1390,12 +1384,6 @@ importers:
'@deepseek-ai/dsh-subagent':
specifier: workspace:^
version: link:../../subagent/subagent
- '@deepseek-ai/dsh-subagent-claude-code':
- specifier: workspace:^
- version: link:../../subagent/subagent-claude-code
- '@deepseek-ai/dsh-subagent-codex':
- specifier: workspace:^
- version: link:../../subagent/subagent-codex
'@deepseek-ai/dsh-subagent-fork':
specifier: workspace:^
version: link:../../subagent/subagent-fork
From 86c51704cb1c5044400fcae25e8e11a87934ec01 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Wed, 12 Aug 2026 18:09:04 +0800
Subject: [PATCH 002/134] fix(bundle): exclude product subagents from base
---
...ludes-product-subagent-providers.i18n.yaml | 6 +++++
...dsh-excludes-product-subagent-providers.md | 25 +++++++++++++++++++
...-excludes-product-subagent-providers.zh.md | 25 +++++++++++++++++++
apps/cli/composition.md | 6 -----
examples/package.json | 2 ++
packages/bundle/base/cordis.patch.yml | 9 -------
packages/bundle/base/tests/base.spec.ts | 10 +++-----
pnpm-lock.yaml | 6 +++++
8 files changed, 68 insertions(+), 21 deletions(-)
create mode 100644 .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
create mode 100644 .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
create mode 100644 .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
new file mode 100644
index 0000000000..44f197e39d
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/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
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
new file mode 100644
index 0000000000..3e3e4fbefb
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
@@ -0,0 +1,25 @@
+# Agent Note: Production dsh excludes product subagent providers
+
+Status: implemented
+
+English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)
+
+## Problem
+
+`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code, including the Claude Agent SDK, even when neither integration is used.
+
+## 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.
+
+## 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.
+
+## Alternatives considered
+
+**Keep dormant providers in the base bundle.** Dormant providers start no product processes, but their packages still enter every production npm install.
+
+## Consequences
+
+Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. Using either integration requires explicit Profile configuration.
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
new file mode 100644
index 0000000000..166964675b
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
@@ -0,0 +1,25 @@
+# Agent Note: 生产 dsh 排除产品 subagent 提供方
+
+Status: implemented
+
+[English](2026-08-12-production-dsh-excludes-product-subagent-providers.md) | 中文
+
+## 问题
+
+`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码,包括 Claude Agent SDK,即使用户并未使用任一集成。
+
+## 决策
+
+本决策取代[共享 host 放置决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md):`@deepseek-ai/dsh-base` 不依赖也不挂载 Codex 与 Claude Code subagent 提供方。需要这些集成的 Profile 仍可显式安装并挂载对应包。仓库 examples 保留直接开发依赖,使其显式提供方配置可以继续解析。
+
+## 验证
+
+base 组合包测试会拒绝这两个提供方依赖与配置行。Cordis 配置验证要求显式 examples 声明其引用的提供方包。
+
+## 考虑过的替代方案
+
+**在 base 组合包中保留休眠提供方。** 休眠提供方不会启动产品进程,但其包仍会进入每次生产 NPM 安装。
+
+## 后果
+
+安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。使用任一集成都需要显式 Profile 配置。
diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index 3e08e97b37..dbac0d8770 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -114,10 +114,6 @@ flowchart LR
cfg --> plugin_dsh_base_subagent_spawn
plugin_dsh_base_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"]
cfg --> plugin_dsh_base_subagent_fork
- plugin_dsh_base_subagent_codex["subagent-codex
@deepseek-ai/dsh-subagent-codex"]
- cfg --> plugin_dsh_base_subagent_codex
- plugin_dsh_base_subagent_claude_code["subagent-claude-code
@deepseek-ai/dsh-subagent-claude-code"]
- cfg --> plugin_dsh_base_subagent_claude_code
plugin_dsh_base_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"]
cfg --> plugin_dsh_base_tool_subagent_control
plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents
@deepseek-ai/dsh-tool-subagent-control/list-agents"]
@@ -225,8 +221,6 @@ flowchart LR
| `subagent` | `@deepseek-ai/dsh-subagent` |
| `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` |
| `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` |
-| `subagent-codex` | `@deepseek-ai/dsh-subagent-codex` |
-| `subagent-claude-code` | `@deepseek-ai/dsh-subagent-claude-code` |
| `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` |
| `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` |
| `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` |
diff --git a/examples/package.json b/examples/package.json
index 761dd0851f..29818133ef 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -72,6 +72,8 @@
"@deepseek-ai/dsh-spill-policy": "workspace:*",
"@deepseek-ai/dsh-subagent": "workspace:*",
"@deepseek-ai/dsh-subagent-acp": "workspace:*",
+ "@deepseek-ai/dsh-subagent-claude-code": "workspace:*",
+ "@deepseek-ai/dsh-subagent-codex": "workspace:*",
"@deepseek-ai/dsh-subagent-dsh-sdk": "workspace:*",
"@deepseek-ai/dsh-subagent-fork": "workspace:*",
"@deepseek-ai/dsh-subagent-spawn": "workspace:*",
diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml
index c276c886dd..093c4026ea 100644
--- a/packages/bundle/base/cordis.patch.yml
+++ b/packages/bundle/base/cordis.patch.yml
@@ -296,15 +296,6 @@
config:
providerName: fork
- # Product providers stay on the host plane because the registry is a
- # process singleton. Agent presets decide whether their own model sees the
- # matching delegation tools; loading either provider starts no product.
- - id: subagent-codex
- name: '@deepseek-ai/dsh-subagent-codex'
-
- - id: subagent-claude-code
- name: '@deepseek-ai/dsh-subagent-claude-code'
-
# Continuable background children are selected per delegation tool. The
# separately loaded follow-up tool registers the one global `send_message`.
- id: tool-subagent-control
diff --git a/packages/bundle/base/tests/base.spec.ts b/packages/bundle/base/tests/base.spec.ts
index 81ceb340d5..23a564fbcf 100644
--- a/packages/bundle/base/tests/base.spec.ts
+++ b/packages/bundle/base/tests/base.spec.ts
@@ -35,12 +35,10 @@ describe('dsh-base bundle', () => {
expect(rows.find(row => row.id === 'telemetry-otel')?.config?.['mode']).toEqual({
__jsExpr: "process.env.DSH_TELEMETRY_MODE || 'DISABLED'",
})
- expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(1)
- expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(1)
- expect(manifest.dependencies).toMatchObject({
- '@deepseek-ai/dsh-subagent-codex': 'workspace:^',
- '@deepseek-ai/dsh-subagent-claude-code': 'workspace:^',
- })
+ expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(0)
+ expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(0)
+ expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
+ expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-claude-code')
})
it('gates each shell stack by platform with a symmetric disabled expression', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 306cd99243..4a2d4a5838 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -608,6 +608,12 @@ importers:
'@deepseek-ai/dsh-subagent-acp':
specifier: workspace:*
version: link:../packages/subagent/subagent-acp
+ '@deepseek-ai/dsh-subagent-claude-code':
+ specifier: workspace:*
+ version: link:../packages/subagent/subagent-claude-code
+ '@deepseek-ai/dsh-subagent-codex':
+ specifier: workspace:*
+ version: link:../packages/subagent/subagent-codex
'@deepseek-ai/dsh-subagent-dsh-sdk':
specifier: workspace:*
version: link:../packages/subagent/subagent-dsh-sdk
From d1629eed45062dd7b1de73e1f0dd51452733d6a8 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Wed, 12 Aug 2026 19:28:31 +0800
Subject: [PATCH 003/134] feat(subagent): make product providers directly
installable
---
...026-08-05-profile-plugin-bundles.i18n.yaml | 4 +-
.../2026-08-05-profile-plugin-bundles.md | 2 +-
.../2026-08-05-profile-plugin-bundles.zh.md | 2 +-
...ubagent-providers-in-shared-host.i18n.yaml | 4 +-
...oduct-subagent-providers-in-shared-host.md | 20 ++-
...ct-subagent-providers-in-shared-host.zh.md | 20 ++-
...code-and-codex-subagent-backends.i18n.yaml | 4 +-
...claude-code-and-codex-subagent-backends.md | 6 +-
...ude-code-and-codex-subagent-backends.zh.md | 6 +-
...ludes-product-subagent-providers.i18n.yaml | 4 +-
...dsh-excludes-product-subagent-providers.md | 10 +-
...-excludes-product-subagent-providers.zh.md | 10 +-
.../agent-presets/code/agent.cordis.yml | 2 +
.../agent-presets/cordis/agent.cordis.yml | 2 +
.../editing-cordis-compositions/SKILL.md | 11 +-
.../agent-presets/standard/agent.cordis.yml | 2 +
apps/cli/reference/README.i18n.yaml | 4 +-
apps/cli/reference/README.md | 12 ++
apps/cli/reference/README.zh.md | 12 ++
apps/cli/tests/built-bin.e2e.ts | 15 +++
apps/cli/tests/web-agent-presets.e2e.ts | 126 ++++++++++++------
docs/module-graph.i18n.yaml | 4 +-
docs/module-graph.md | 15 +--
docs/module-graph.zh.md | 15 +--
.../subagent/subagent-claude-code/cordis.yml | 18 +--
.../subagent/subagent-claude-code/driver.ts | 57 +++-----
.../subagent/subagent-codex/cordis.yml | 7 +-
.../subagent/subagent-codex/driver.ts | 9 +-
packages/bundle/README.i18n.yaml | 4 +-
packages/bundle/README.md | 2 +
packages/bundle/README.zh.md | 2 +
packages/bundle/base/README.i18n.yaml | 4 +-
packages/bundle/base/README.md | 3 +-
packages/bundle/base/README.zh.md | 3 +-
packages/subagent/README.i18n.yaml | 4 +-
packages/subagent/README.md | 2 +
packages/subagent/README.zh.md | 2 +
.../subagent-claude-code/README.i18n.yaml | 4 +-
.../subagent/subagent-claude-code/README.md | 15 ++-
.../subagent-claude-code/README.zh.md | 15 ++-
.../subagent-claude-code/cordis.patch.yml | 6 +
.../subagent-claude-code/package.json | 7 +
.../tests/loader-composition.e2e.ts | 64 ++++-----
.../tests/subagent-claude-code.spec.ts | 29 ++++
.../subagent/subagent-codex/README.i18n.yaml | 4 +-
packages/subagent/subagent-codex/README.md | 15 ++-
packages/subagent/subagent-codex/README.zh.md | 15 ++-
.../subagent/subagent-codex/cordis.patch.yml | 6 +
packages/subagent/subagent-codex/package.json | 10 +-
.../tests/loader-composition.e2e.ts | 9 ++
.../tests/subagent-codex.spec.ts | 31 +++++
pnpm-lock.yaml | 6 +-
scripts/check-workspace-constraints.ts | 2 +
.../verify-config-source-ownership.spec.ts | 4 +-
scripts/verify-config-source-ownership.ts | 3 +-
scripts/verify-cordis-config.spec.ts | 98 +++++++++++++-
scripts/verify-cordis-config.ts | 60 +++++++--
57 files changed, 583 insertions(+), 249 deletions(-)
create mode 100644 packages/subagent/subagent-claude-code/cordis.patch.yml
create mode 100644 packages/subagent/subagent-codex/cordis.patch.yml
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
index 28d5fad868..46c178ec65 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.i18n.yaml
+++ b/.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
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
index ffef7b67a1..fdc92eec39 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
+++ b/.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/` 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 ` 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 ` 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 ` 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 ` 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).
diff --git a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
index b190d5e834..51bb84d9b3 100644
--- a/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.zh.md
@@ -12,7 +12,7 @@ Status: implemented
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/`,其中包含一个 `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 ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 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 ` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch`。`dsh plugin --profile ` 是一层薄薄的 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`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index b696b46749..ba797746c5 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index 33b6eb6cf7..6db6ca6655 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.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.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index fd78c7a3fe..e6c221299d 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.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 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 84cb091651..ed38bf3472 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.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
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index ccc96d6c99..80dc7ad848 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.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.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index 740eeb633e..c6b5f89067 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.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 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
index 44f197e39d..d021cfbab7 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
+++ b/.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
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
index 3e3e4fbefb..94cfe82d0a 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
+++ b/.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.
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
index 166964675b..a9dbdcff74 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
+++ b/.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、动态安装程序或持久化的产品启用状态。
diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml
index 992b1a3eb5..ae2e0b2653 100644
--- a/apps/cli/config/agent-presets/code/agent.cordis.yml
+++ b/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
diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
index d05063846f..03fde2c612 100644
--- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml
+++ b/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
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index d897cafb7a..c3ee75b143 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/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 add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile 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 remove ` 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
diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml
index b57b18eef7..ded4a41ab5 100644
--- a/apps/cli/config/agent-presets/standard/agent.cordis.yml
+++ b/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
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 309d184da5..2cb82986a3 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index 17d63fe73e..ed6a645c85 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` 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 add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile 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
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index ee99592fbb..a75ef3426f 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -42,6 +42,18 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `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 add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile 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
diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts
index c989d9ce3e..d4bcb91f6a 100644
--- a/apps/cli/tests/built-bin.e2e.ts
+++ b/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
+ 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 })
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index fcbb00d33f..dfd86a566d 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/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 {
+async function bootWeb(
+ settingsFile: string,
+ extra: PatchOptions[] = [],
+ profilePackages: readonly string[] = [],
+): Promise {
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 {
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)
+ ], installed.map(product => dirname(product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH)))
+ }
- afterAll(async () => {
- await productCtx.fiber.dispose()
- })
-
- it('composes none, either product, or both without changing the shared host registry', async () => {
- const expected = new Map([
+ it('composes the intersection of installed Bundles and enabled preset rows', async () => {
+ const enabledByPreset = new Map([
['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',
- ]))
+ const installations: Product[][] = [
+ [],
+ ['codex'],
+ ['claude-code'],
+ ['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),
- })
+ 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', () => {
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index d5ac47cf78..6277e1e081 100644
--- a/docs/module-graph.i18n.yaml
+++ b/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
diff --git a/docs/module-graph.md b/docs/module-graph.md
index 56e029df19..4518321070 100644
--- a/docs/module-graph.md
+++ b/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) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index 839c5edf75..3516346d8c 100644
--- a/docs/module-graph.zh.md
+++ b/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) |
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
index fcdc0c4fb6..10ccb77e4d 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
+++ b/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:
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts
index d7540e1a2d..72e2fb5482 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts
+++ b/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 {
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
index e770b11c95..45b601aed7 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
+++ b/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:
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
index 873f5e36de..af54c5fc0c 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
+++ b/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
diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml
index eafbe0b0ab..0441ec7d87 100644
--- a/packages/bundle/README.i18n.yaml
+++ b/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
diff --git a/packages/bundle/README.md b/packages/bundle/README.md
index 696aa9ef7b..4d7a064939 100644
--- a/packages/bundle/README.md
+++ b/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) |
diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md
index 2bb22c7949..8910b33a97 100644
--- a/packages/bundle/README.zh.md
+++ b/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) |
diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml
index 3e15c33837..895dcb6994 100644
--- a/packages/bundle/base/README.i18n.yaml
+++ b/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
diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md
index bd38f39f58..a963bcca67 100644
--- a/packages/bundle/base/README.md
+++ b/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 (`\dsh-`, TMP/TEMP rewritten for confined children); `read-only` grants nothing. See `@deepseek-ai/dsh-sandbox-windows-acl`.
diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md
index 2c6ff8513b..d6594dd35c 100644
--- a/packages/bundle/base/README.zh.md
+++ b/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 子目录(`\dsh-`,受限子进程的 TMP/TEMP 被改写);`read-only` 不授予任何写入。见 `@deepseek-ai/dsh-sandbox-windows-acl`。
diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml
index 10ad9837b2..600d1b909d 100644
--- a/packages/subagent/README.i18n.yaml
+++ b/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
diff --git a/packages/subagent/README.md b/packages/subagent/README.md
index 134028c999..870cb4be0d 100644
--- a/packages/subagent/README.md
+++ b/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 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.
diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md
index ab4284d052..122c34c264 100644
--- a/packages/subagent/README.zh.md
+++ b/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 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。
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index bbb3c9cf1a..38d5ddb93a 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/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
diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md
index 17b14e847b..b1a5c4bb4b 100644
--- a/packages/subagent/subagent-claude-code/README.md
+++ b/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 add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
+dsh --profile
+```
+
+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//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
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index 2f59144d5b..0b5419c3b0 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/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 add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
+dsh --profile
+```
+
+安装决定 Host 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_claude_code`。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
```yaml
+# $DSH_HOME/profiles//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
diff --git a/packages/subagent/subagent-claude-code/cordis.patch.yml b/packages/subagent/subagent-claude-code/cordis.patch.yml
new file mode 100644
index 0000000000..63c0319626
--- /dev/null
+++ b/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'
diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json
index 7966835ccd..8e1b531c2d 100644
--- a/packages/subagent/subagent-claude-code/package.json
+++ b/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:^",
diff --git a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts
index 51a2ea0025..7ad1871e7a 100644
--- a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts
+++ b/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)
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index cee479fd9d..953445c2e1 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/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
+ exports?: Record
+ 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' },
diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml
index 1cfb9645c2..459b1de1a4 100644
--- a/packages/subagent/subagent-codex/README.i18n.yaml
+++ b/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
diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md
index 3d59ca1eaf..18e805f0e0 100644
--- a/packages/subagent/subagent-codex/README.md
+++ b/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 add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh --profile
+```
+
+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//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
diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md
index b60cb1bba9..ff22fe1510 100644
--- a/packages/subagent/subagent-codex/README.zh.md
+++ b/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 add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh --profile
+```
+
+安装决定 Host 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_codex`。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
```yaml
+# $DSH_HOME/profiles//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
diff --git a/packages/subagent/subagent-codex/cordis.patch.yml b/packages/subagent/subagent-codex/cordis.patch.yml
new file mode 100644
index 0000000000..75e7464574
--- /dev/null
+++ b/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'
diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json
index 6901a6d20b..badc4f1b50 100644
--- a/packages/subagent/subagent-codex/package.json
+++ b/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:^",
diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
index 6c4019f8c8..afdec98305 100644
--- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
+++ b/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.
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 81923f2228..50a98cf0a6 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/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
+ peerDependencies?: Record
+ exports?: Record
+ 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',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a2d4a5838..7ef83149d7 100644
--- a/pnpm-lock.yaml
+++ b/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
diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts
index 263781ce5a..ef389a3cb1 100644
--- a/scripts/check-workspace-constraints.ts
+++ b/scripts/check-workspace-constraints.ts
@@ -135,6 +135,8 @@ const packageFileExtras: Readonly> = {
'@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.
diff --git a/scripts/verify-config-source-ownership.spec.ts b/scripts/verify-config-source-ownership.spec.ts
index 41026c5fe4..0c675c4f1a 100644
--- a/scripts/verify-config-source-ownership.spec.ts
+++ b/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.',
])
diff --git a/scripts/verify-config-source-ownership.ts b/scripts/verify-config-source-ownership.ts
index 0684124215..c42e7cb793 100644
--- a/scripts/verify-config-source-ownership.ts
+++ b/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',
]
diff --git a/scripts/verify-cordis-config.spec.ts b/scripts/verify-cordis-config.spec.ts
index 6c1304e16a..e889209516 100644
--- a/scripts/verify-cordis-config.spec.ts
+++ b/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
+ optionalDependencies?: Record
+ peerDependencies?: Record
+}
+
+const repoRoot = fileURLToPath(new URL('..', import.meta.url))
+
+function productionClosure(entry: string): Set {
+ const manifests = new Map()
+ 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()
+ 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')
+ })
+})
diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts
index bb10db0c70..62e641996e 100644
--- a/scripts/verify-cordis-config.ts
+++ b/scripts/verify-cordis-config.ts
@@ -20,12 +20,14 @@ interface JsExpr {
__jsExpr: string
}
-interface PackageManifest {
+export interface PackageManifest {
name?: string
dependencies?: Record
+ optionalDependencies?: Record
+ 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 {
From 208f37157abc37964d3fffee3a9c44cafa84bc33 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Thu, 13 Aug 2026 16:41:25 +0800
Subject: [PATCH 004/134] fix(subagent): simplify optional provider delivery
---
...ludes-product-subagent-providers.i18n.yaml | 4 +--
...dsh-excludes-product-subagent-providers.md | 2 +-
...-excludes-product-subagent-providers.zh.md | 2 +-
.../editing-cordis-compositions/SKILL.md | 2 +-
apps/cli/reference/README.i18n.yaml | 4 +--
apps/cli/reference/README.md | 2 +-
apps/cli/reference/README.zh.md | 2 +-
apps/cli/tests/web-agent-presets.e2e.ts | 28 ++++++++++---------
examples/acp-agent/tests/acp.snapshot.ts | 13 ++++++++-
.../tests/snapshots/skill-load/input.json | 2 +-
.../tests/snapshots/skill-load/session.jsonl | 18 ++++++------
.../subagent-claude-code/package.json | 1 -
.../tests/subagent-claude-code.spec.ts | 2 --
packages/subagent/subagent-codex/package.json | 1 -
.../tests/subagent-codex.spec.ts | 2 --
scripts/check-workspace-constraints.ts | 18 +++++++-----
16 files changed, 57 insertions(+), 46 deletions(-)
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
index d021cfbab7..7d14cbe963 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
+++ b/.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: 94cfe82d0aa42076f3c0723ed99a83a1e53e3724
-2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a9dbdcff748ea46dc84c1480e6e50a945219a5c7
+2026-08-12-production-dsh-excludes-product-subagent-providers.md: 53551ad06ce7b735620d605669ddb5ca2d20aef5
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a5ff017e2717dee326e8aaf7987ab8360452902f
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
index 94cfe82d0a..53551ad06c 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
@@ -16,7 +16,7 @@ The two Bundles remain independent. The Codex Bundle owns its `@deepseek-ai/dsh-
## Verification
-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.
+Package tests pin each Bundle manifest, published patch, exact self-provider row, and product-specific runtime dependency. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. 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, the full tool-grant matrix on a Host with both providers, representative missing-provider cases, and zero product processes. The base bundle test continues to reject both provider dependencies and configuration rows.
## Alternatives considered
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
index a9dbdcff74..a5ff017e27 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
@@ -16,7 +16,7 @@ Status: implemented
## 验证
-包测试会固定每个 Bundle 的 manifest、导出的 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会按 Bundle 声明发现 manifest,而非按目录发现。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合与四种工具集合的完整矩阵,并证明组装不会启动产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
+包测试会固定每个 Bundle 的 manifest、发布 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合、同时安装两个提供方时的完整工具授权矩阵、缺失提供方的代表场景以及零产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
## 考虑过的替代方案
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index 2eff9a9cce..68999a7ed6 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
@@ -154,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 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.
+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 starts, authenticates, selects a model for, probes, or manages a host-level installation of either product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`.
## What not to move into a preset
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 5b97a26ad2..6d2f457b01 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: c220fd68cb0d79d2060e46a33d8af54c249a9c68
-README.zh.md: 15f026c79665ae2978bdfd65c321b05c10cc06e4
+README.md: dcbe28fab05031b2f176e5a20df77967d2d97885
+README.zh.md: 0cd7216615a58b206b441096292e1a19a73a01b3
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index c220fd68cb..dcbe28fab0 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile 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.
+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, authenticate, configure, or manage a host-level installation of the native product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`. 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
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index 15f026c796..0cd7216615 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile 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。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动、认证、配置原生产品,也不会管理宿主级产品安装。Claude Code Bundle 的 Agent SDK 依赖仍携带平台 CLI 载荷,但生产环境会忽略该载荷并使用宿主提供的 `claude`。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 5da62e0aba..77185270e9 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -437,6 +437,7 @@ describe('the shipped Web composition', () => {
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'
+ type PresetId = typeof presetIds[number]
async function bootProducts(installed: readonly Product[]): Promise {
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
@@ -477,20 +478,20 @@ describe('product subagent Bundle and user-preset intersection', () => {
}
it('composes the intersection of installed Bundles and enabled preset rows', async () => {
- const enabledByPreset = new Map([
- ['products-none', []],
- ['products-codex', ['codex']],
- ['products-claude', ['claude-code']],
- ['products-both', ['codex', 'claude-code']],
- ])
- const installations: Product[][] = [
- [],
- ['codex'],
- ['claude-code'],
- ['codex', 'claude-code'],
+ const enabledByPreset: Record = {
+ 'products-none': [],
+ 'products-codex': ['codex'],
+ 'products-claude': ['claude-code'],
+ 'products-both': ['codex', 'claude-code'],
+ }
+ const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
+ { installed: [], presets: ['products-both'] },
+ { installed: ['codex'], presets: ['products-both'] },
+ { installed: ['claude-code'], presets: ['products-both'] },
+ { installed: ['codex', 'claude-code'], presets: presetIds },
]
- for (const installed of installations) {
+ for (const { installed, presets } of scenarios) {
const productCtx = await bootProducts(installed)
const spawn = vi.spyOn(productCtx.subprocess, 'spawn')
try {
@@ -498,7 +499,8 @@ describe('product subagent Bundle and user-preset intersection', () => {
.filter(name => name === 'codex' || name === 'claude-code')
.sort())
.toEqual([...installed].sort())
- for (const [id, enabled] of enabledByPreset) {
+ for (const id of presets) {
+ const enabled = enabledByPreset[id]
const handle = await productCtx.agents.create({
sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts
index db4a2b5d2f..19bb86c298 100644
--- a/examples/acp-agent/tests/acp.snapshot.ts
+++ b/examples/acp-agent/tests/acp.snapshot.ts
@@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
-import { mkdir, utimes, writeFile } from 'node:fs/promises'
+import { copyFile, mkdir, utimes, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { homedir } from 'node:os'
import { expect, it } from 'vitest'
@@ -28,6 +28,10 @@ const AGENT = {
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
}
+const EDITING_CORDIS_SKILL = fileURLToPath(new URL(
+ '../../../apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md',
+ import.meta.url,
+))
// The Code Mode overlay configs (include-patched variants of cordis.yml; the
// replay swap resolves each one's sibling `*cordis.snapshot.yml`).
@@ -69,6 +73,12 @@ const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml',
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
+async function prepareEditingCordisSkillWorkspace(cwd: string): Promise {
+ const target = join(cwd, '.dsh', 'skills', 'editing-cordis-compositions', 'SKILL.md')
+ await mkdir(dirname(target), { recursive: true })
+ await copyFile(EDITING_CORDIS_SKILL, target)
+}
+
async function prepareDelimiterPathWorkspace(cwd: string): Promise {
const dir = join(cwd, 'scope')
await mkdir(dir, { recursive: true })
@@ -280,6 +290,7 @@ const SCENARIOS: Scenario[] = [
headerClass: 'skill',
systemPromptSource: 'text-turn',
toolSchemasSource: 'text-turn',
+ prepareWorkspace: prepareEditingCordisSkillWorkspace,
},
{ name: 'lsp-definition', hasModelTurn: true, recorded: false, pinsHeader: true, headerClass: 'lsp', configPath: LSP_CONFIG },
// web_fetch markdown rendering end to end: the overlay's loopback fixture
diff --git a/examples/acp-agent/tests/snapshots/skill-load/input.json b/examples/acp-agent/tests/snapshots/skill-load/input.json
index a5ee78bff6..48235fc667 100644
--- a/examples/acp-agent/tests/snapshots/skill-load/input.json
+++ b/examples/acp-agent/tests/snapshots/skill-load/input.json
@@ -2,6 +2,6 @@
"steps": [
{ "op": "initialize" },
{ "op": "newSession" },
- { "op": "prompt", "text": "Load the snapshot-skill skill with the skill tool, then reply DONE." }
+ { "op": "prompt", "text": "Load the editing-cordis-compositions skill with the skill tool, then reply DONE." }
]
}
diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
index ec369b492a..0f9608ae4c 100644
--- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
@@ -1,25 +1,25 @@
{"type":"session","version":0,"id":"9eb4181f-2d05-49d3-98fc-3711fe2f5664","createdAt":1783654655599,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498773710,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"0ca31b92-27ac-451d-98d3-d1e5f605454b"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498773710,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Load the editing-cordis-compositions skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"0ca31b92-27ac-451d-98d3-d1e5f605454b"}]}}
{"type":"turn/start","seq":1,"time":1785821378605,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785821378605,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","seq":3,"time":1785498773754,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":4,"time":1785498773754,"data":{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"0ca31b92-27ac-451d-98d3-d1e5f605454b"},"surfaceOp":"append"}
+{"type":"user/message","seq":4,"time":1785498773754,"data":{"content":[{"type":"text","text":"Load the editing-cordis-compositions skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"0ca31b92-27ac-451d-98d3-d1e5f605454b"},"surfaceOp":"append"}
{"type":"user/message","seq":5,"time":1785498773755,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"3fc7e2f8-90fc-496c-b516-700cef1d86f1"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730426818,"data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n"}],"source":{"kind":"skill-catalog","form":"catalog","entries":[{"name":"model-only-skill","description":"Prove user-disabled skills remain available to the model."},{"name":"snapshot-skill","description":"Exercise project skill discovery and loading in snapshot tests."}]},"role":"user","id":"60880315-9799-44c8-8a99-e6fe9ee5bdc5"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730426818,"data":{"title":"Load the snapshot-skill skill with","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"user/message","seq":6,"time":1785730426818,"data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `editing-cordis-compositions`: Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing.\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n"}],"source":{"kind":"skill-catalog","form":"catalog","entries":[{"name":"editing-cordis-compositions","description":"Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing."},{"name":"model-only-skill","description":"Prove user-disabled skills remain available to the model."},{"name":"snapshot-skill","description":"Exercise project skill discovery and loading in snapshot tests."}]},"role":"user","id":"59831057-0914-4e8b-967d-ef7dc850a62a"},"surfaceOp":"append"}
+{"type":"session/title","seq":7,"time":1785730426818,"data":{"title":"Load the editing-cordis-compositions ski","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":8,"time":1785498773756,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":9,"time":1785730426819,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":10,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"assistant/chunk","seq":11,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}}
{"type":"assistant/chunk","seq":12,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":13,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"snapshot-skill\"}"}}}
+{"type":"assistant/chunk","seq":13,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"editing-cordis-compositions\"}"}}}
{"type":"assistant/chunk","seq":14,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}}}
-{"type":"assistant/chunk","seq":15,"time":1784903324935,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}}}}
+{"type":"assistant/chunk","seq":15,"time":1784903324935,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}}}
{"type":"assistant/chunk","seq":16,"time":1785498773765,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}}
{"type":"assistant/chunk","seq":17,"time":1785730426828,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":18,"time":1785730426828,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fd7a47e-84c9-4d31-aa95-9939671ba0a5"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[10,11,12,13,14,15,16,17],"surfaceOp":"append"}
-{"type":"tool/call","seq":19,"time":1785730426828,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}}
-{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/snapshot-skill\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\nFollow these snapshot-only instructions.\nResolve referenced resources relative to this skill directory.\n\n"}],"isError":false}],"role":"user","id":"1609c2f6-3bc5-4ade-95dd-29e7f7565987"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
+{"type":"assistant/message","seq":18,"time":1785730426828,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fd7a47e-84c9-4d31-aa95-9939671ba0a5"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[10,11,12,13,14,15,16,17],"surfaceOp":"append"}
+{"type":"tool/call","seq":19,"time":1785730426828,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}
+{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nCodex 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:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe 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 remove ` withdraws only that provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n enableRunInBackground: false\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false\n maxDepth: provider-managed\n```\n\nThe 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 starts, authenticates, selects a model for, probes, or manages a host-level installation of either product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"fa340fc0-3edc-4a61-92b2-2c4d70c4b6d7"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","seq":21,"time":1785730426838,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":22,"time":1785730426848,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json
index 473a37e3d8..e62b68ef3d 100644
--- a/packages/subagent/subagent-claude-code/package.json
+++ b/packages/subagent/subagent-claude-code/package.json
@@ -22,7 +22,6 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
- "./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./package.json": "./package.json"
},
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index 889e248983..0fb0e55a98 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
@@ -290,12 +290,10 @@ describe('task admission and package contracts', () => {
const root = fileURLToPath(new URL('..', import.meta.url))
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
dependencies?: Record
- exports?: Record
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')
diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json
index 246f59348e..fd4a3c3611 100644
--- a/packages/subagent/subagent-codex/package.json
+++ b/packages/subagent/subagent-codex/package.json
@@ -22,7 +22,6 @@
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
- "./cordis.patch.yml": "./cordis.patch.yml",
"./src/*": "./src/*",
"./package.json": "./package.json"
},
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 05207c419a..4fa001a9f4 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -269,12 +269,10 @@ describe('task admission and package contracts', () => {
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
dependencies?: Record
peerDependencies?: Record
- exports?: Record
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')
diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts
index d55ad16401..e11672a055 100644
--- a/scripts/check-workspace-constraints.ts
+++ b/scripts/check-workspace-constraints.ts
@@ -84,6 +84,11 @@ interface PackageManifest {
devDependencies?: Record
dependencies?: Record
optionalDependencies?: Record
+ dsh?: {
+ bundle?: {
+ patch?: string
+ }
+ }
}
/** One workspace manifest and its repo-relative path. */
@@ -131,12 +136,6 @@ function workspaceManifests(): WorkspaceManifest[] {
}
const packageFileExtras: Readonly> = {
- // Profile bundles publish their dsh.bundle.patch layer beside the lib.
- '@deepseek-ai/dsh-base': ['cordis.patch.yml'],
- '@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
- '@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
- '@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.
@@ -154,7 +153,12 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
}
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
- const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
+ const declaredPatch = manifest.dsh?.bundle?.patch
+ const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
+ const extras = [
+ ...bundleFiles,
+ ...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
+ ]
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
From bb3010a8dfb406a472115052754591c58ce09fa8 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Thu, 13 Aug 2026 18:45:16 +0800
Subject: [PATCH 005/134] fix(subagent): use bundled Claude Code CLI
---
...ubagent-providers-in-shared-host.i18n.yaml | 4 +-
...oduct-subagent-providers-in-shared-host.md | 6 +-
...ct-subagent-providers-in-shared-host.zh.md | 6 +-
...code-and-codex-subagent-backends.i18n.yaml | 4 +-
...claude-code-and-codex-subagent-backends.md | 6 +-
...ude-code-and-codex-subagent-backends.zh.md | 6 +-
...ludes-product-subagent-providers.i18n.yaml | 4 +-
...dsh-excludes-product-subagent-providers.md | 8 +-
...-excludes-product-subagent-providers.zh.md | 8 +-
.../editing-cordis-compositions/SKILL.md | 2 +-
apps/cli/reference/README.i18n.yaml | 4 +-
apps/cli/reference/README.md | 2 +-
apps/cli/reference/README.zh.md | 2 +-
apps/web/tests/skill-tool-row.e2e.ts | 10 +-
.../snapshots/skill-tool-row/ui.expected.md | 10 +-
.../tests/snapshots/skill-load/session.jsonl | 2 +-
.../subagent-claude-code/README.i18n.yaml | 4 +-
.../subagent/subagent-claude-code/README.md | 16 ++--
.../subagent-claude-code/README.zh.md | 16 ++--
.../subagent-claude-code/src/index.ts | 6 --
.../subagent-claude-code/src/process.ts | 18 +---
.../subagent/subagent-claude-code/src/run.ts | 3 -
.../tests/real-deepseek.e2e.ts | 3 +-
.../tests/real-product.spec.ts | 40 +++-----
.../tests/subagent-claude-code.spec.ts | 91 +++++++++++++------
25 files changed, 144 insertions(+), 137 deletions(-)
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index ba797746c5..e8572ed53d 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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: 6db6ca665532ec2b457859243fee5cdc750e954b
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: e6c221299d80e6e66858db607c6b4696942b8a61
+2026-08-10-product-subagent-providers-in-shared-host.md: 2b1a417f7e751b2edee7e3b23dd439feab1353ea
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: 2f687b7dc9332680b8aa610f46668f197066c517
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index 6db6ca6655..2b1a417f7e 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -16,15 +16,15 @@ When installed in a Profile, each product Bundle loads its fixed `codex` or `cla
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. 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.
+The providers have different executable owners. Codex starts a host `codex` from `PATH`. The Claude Code Bundle installs its pinned Agent SDK and matching platform CLI; the provider lets that SDK choose the private native executable and passes the command through the shared subprocess owner without consulting or falling back to a host `claude`. Loading either Bundle only registers the provider and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing Codex command or Claude platform payload, authentication failure, and other product failures remain local to the attempted delegation.
## Verification
-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.
+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 host executable resolution for Codex, SDK platform-payload selection without fallback for Claude Code, failure, cancellation, and process-tree quiescence.
## Alternatives considered
-**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.
+**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, the Claude Agent SDK, and its large platform CLI payload 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.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index e6c221299d..2f687b7dc9 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -16,15 +16,15 @@ Status: implemented
[生产依赖闭包决策](../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。加载 Bundle 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
+两个提供方的可执行文件归属不同。Codex 会启动从 `PATH` 解析出的宿主 `codex`。Claude Code Bundle 会安装锁定的 Agent SDK 与匹配平台 CLI;提供方让 SDK 选择该私有原生可执行文件,再把命令交给共享子进程责任方,既不查询也不回退宿主 `claude`。加载任一 Bundle 只会注册提供方,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。Codex 命令缺失、Claude 平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
## 验证
-真实组装会加载未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装这四种集合,并与不授权工具、仅授权其中一个或同时授权两者的 Agent Preset 完整交叉。测试证明 Host 注册表等于已安装 Bundle 集合,模型可见工具等于已安装且已授权集合的交集,并且组装期间不会启动产品进程。Preset 编辑覆盖继续证明代际隔离。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
+真实组装会加载未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装这四种集合,并与不授权工具、仅授权其中一个或同时授权两者的 Agent Preset 完整交叉。测试证明 Host 注册表等于已安装 Bundle 集合,模型可见工具等于已安装且已授权集合的交集,并且组装期间不会启动产品进程。Preset 编辑覆盖继续证明代际隔离。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则分别证明 Codex 的宿主可执行文件解析、Claude Code 的 SDK 平台载荷选择与无回退行为,以及失败、取消和进程树完全停稳。
## 考虑过的替代方案
-**在每个 base Profile 中保留两个休眠提供方。** 这样每条匹配的 Preset 行都能立即使用,但即使用户不需要任一集成,每次生产安装仍会携带两个提供方包和 Claude Agent SDK。
+**在每个 base Profile 中保留两个休眠提供方。** 这样每条匹配的 Preset 行都能立即使用,但即使用户不需要任一集成,每次生产安装仍会携带两个提供方包、Claude Agent SDK 及其大型平台 CLI 载荷。
**存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 6bb4a7f909..856c199c8b 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.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: 80dc7ad8488b3ed557361ab3e88948e56763c860
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 705757e26bccc55eab0787eb440720a2b5a67601
+2026-08-04-claude-code-and-codex-subagent-backends.md: f9c7529db664d5b7bebcd77ba69fd974d6cad5b3
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 8b0e42507c5a0850d569bff3c9abe962b4a03fe0
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index 80dc7ad848..f9c7529db6 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
@@ -47,7 +47,7 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
## Claude Code provider
-`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
+`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. The provider omits `pathToClaudeCodeExecutable`, so the SDK selects Claude Code 2.1.220 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host `claude`; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official `query()` entrypoint and passes the SDK's native `claude` or `claude.exe` command, arguments, cwd, environment, and forwarded signal from `spawnClaudeCodeProcess` to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
The public configuration contains the same two deployment-owned values as the Codex sibling: an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Each run creates its own `AbortController`, sets `persistSession: false`, and disables `AskUserQuestion`. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK rather than waiting for a user interface the provider does not own.
@@ -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. 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 evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader and optional Bundle-composition evidence resolve the selected product packages by name while starting neither product.
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
@@ -89,6 +89,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
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.
+Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Codex behavior depends on the deployment's installed CLI and native configuration; Claude Code behavior depends on the Bundle-pinned platform CLI plus native account 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.
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index 705757e26b..8b0e42507c 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
@@ -47,7 +47,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
## Claude Code 提供方
-`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定名称 `claude`,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
+`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。提供方会省略 `pathToClaudeCodeExecutable`,因此 SDK 会从自己的 optional dependency 闭包中,按操作系统、CPU 与 Linux libc 选择携带 Claude Code 2.1.220 的匹配平台包。提供方既不会解析也不会回退宿主 `claude`;省略 optional dependency、不受支持的平台,以及缺失或损坏的平台载荷,都会在第一次委派的 SDK 启动边界失败。提供方使用官方 `query()` 入口点,并把 SDK 的 `spawnClaudeCodeProcess` 给出的原生 `claude` 或 `claude.exe` 命令、参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
公开配置包含与 Codex 兄弟提供方相同、由部署方负责的两个值:显式的 `env` 覆盖项,以及须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
@@ -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 与可选 Bundle 组装证据会按名称解析已选择的产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
+Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 与可选 Bundle 组装证据会按名称解析已选择的产品包且不启动产品。
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
@@ -89,6 +89,6 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
用户通过官方产品集成支持的两个稳定前台工具进行委派。已安装提供方位于进程级 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 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
+每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。Codex 行为取决于部署环境中安装的 CLI 与原生配置;Claude Code 行为取决于 Bundle 锁定的平台 CLI,以及原生账户和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
index 7d14cbe963..b22e11f805 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
+++ b/.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: 53551ad06ce7b735620d605669ddb5ca2d20aef5
-2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: a5ff017e2717dee326e8aaf7987ab8360452902f
+2026-08-12-production-dsh-excludes-product-subagent-providers.md: b729115ead5ec6823b0c98815fa12f50022defdb
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: ac10ae2b4f04ddc3997b5fe4bbb8f656742de547
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
index 53551ad06c..b729115ead 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
@@ -6,17 +6,17 @@ English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers
## Problem
-`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code, including the Claude Agent SDK, even when neither integration is used.
+`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code, including the Claude Agent SDK and its roughly 250 MB unpacked platform CLI payload, even when neither integration is used.
## Decision
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.
+The two Bundles remain independent. The Codex Bundle owns its `@deepseek-ai/dsh-sdk-protocol` runtime dependency and continues to use a host `codex` from `PATH`. The Claude Code Bundle owns the pinned Agent SDK and the matching platform CLI selected from the SDK's optional dependencies; production uses that private CLI and never falls back to a host `claude`. Installing one Bundle does not pull in the other, and the default `@deepseek-ai/dsh` production closure contains neither provider, the Claude Agent SDK, nor its platform payloads. 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 brings only the selected package closure onto disk; it does not start a product, authenticate an account, rewrite native settings, or grant model access.
## Verification
-Package tests pin each Bundle manifest, published patch, exact self-provider row, and product-specific runtime dependency. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. 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, the full tool-grant matrix on a Host with both providers, representative missing-provider cases, and zero product processes. The base bundle test continues to reject both provider dependencies and configuration rows.
+Package tests pin each Bundle manifest, published patch, exact self-provider row, and product-specific runtime closure. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform package identities and versions, the SDK-selected executable entering the shared subprocess owner, and first-delegation failure without host fallback when the payload is missing. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. 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, the full tool-grant matrix on a Host with both providers, representative missing-provider cases, and zero product processes. The base bundle test continues to reject both provider dependencies and configuration rows.
## Alternatives considered
@@ -26,4 +26,4 @@ Package tests pin each Bundle manifest, published patch, exact self-provider row
## Consequences
-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.
+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. Selecting Claude Code explicitly accepts its SDK and one large platform CLI payload, while selecting Codex does not install a product CLI. 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.
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
index a5ff017e27..ac10ae2b4f 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
@@ -6,17 +6,17 @@ Status: implemented
## 问题
-`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码,包括 Claude Agent SDK,即使用户并未使用任一集成。
+`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码,包括 Claude Agent SDK 及其解包后约 250 MB 的平台 CLI 载荷,即使用户并未使用任一集成。
## 决策
本决策只部分取代[共享 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 是否获得对应工具。安装不会启动产品、验证身份、配置产品或向模型授予任一产品的访问权。
+两个 Bundle 彼此独立。Codex Bundle 自己负责运行时依赖 `@deepseek-ai/dsh-sdk-protocol`,并继续使用 `PATH` 中的宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK,以及从 SDK optional dependencies 中选出的匹配平台 CLI;生产运行只使用该私有 CLI,绝不会回退到宿主 `claude`。安装其中一个 Bundle 不会带入另一个,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含 Claude Agent SDK 或其平台载荷。已安装的 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装只会把所选包闭包放到磁盘上;它不会启动产品、验证账户、改写原生设置或向模型授予访问权。
## 验证
-包测试会固定每个 Bundle 的 manifest、发布 patch、准确的自身提供方行以及产品专属运行时依赖。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合、同时安装两个提供方时的完整工具授权矩阵、缺失提供方的代表场景以及零产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
+包测试会固定每个 Bundle 的 manifest、发布 patch、准确的自身提供方行以及产品专属运行时闭包。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包的身份与版本、SDK 所选可执行文件进入共享子进程责任方的路径,以及载荷缺失时第一次委派失败且不回退宿主 CLI。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合、同时安装两个提供方时的完整工具授权矩阵、缺失提供方的代表场景以及零产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
## 考虑过的替代方案
@@ -26,4 +26,4 @@ Status: implemented
## 后果
-安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除任一提供方包,也可以同时操作两者;Host 可用性的变化会在下次 Profile 启动时生效。单独创作的 Agent Preset 仍只会向新组装的 Session 授予模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。
+安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除任一提供方包,也可以同时操作两者;Host 可用性的变化会在下次 Profile 启动时生效。选择 Claude Code 代表明确接受其 SDK 与一个大型平台 CLI 载荷,而选择 Codex 不会安装产品 CLI。单独创作的 Agent Preset 仍只会向新组装的 Session 授予模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index 68999a7ed6..dcf2194352 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
@@ -154,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 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 starts, authenticates, selects a model for, probes, or manages a host-level installation of either product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`.
+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 provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings.
## What not to move into a preset
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 24ef2b7f35..38a333beea 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: dd4e9801452d6c80dc7054b17c70397536adc05c
-README.zh.md: d029f0f42e2008eddc5ff07177f8eb6ab11fcbba
+README.md: 4bb0502a9af9299fed991bb8a2279d74ccf97037
+README.zh.md: 1b641cefaa4f5ece496fc6af2447e45cd308f395
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index dd4e980145..4bb0502a9a 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile 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, authenticate, configure, or manage a host-level installation of the native product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`. 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.
+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 starts no product process. The Codex provider resolves a host `codex` from `PATH`; the Claude Code Bundle instead installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native product settings remain user-managed for both products; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. 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 or its platform payloads.
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index d029f0f42e..1b641cefaa 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动、认证、配置原生产品,也不会管理宿主级产品安装。Claude Code Bundle 的 Agent SDK 依赖仍携带平台 CLI 载荷,但生产环境会忽略该载荷并使用宿主提供的 `claude`。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动产品进程。Codex provider 会从 `PATH` 解析宿主 `codex`;Claude Code Bundle 则会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。两个产品的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/web/tests/skill-tool-row.e2e.ts b/apps/web/tests/skill-tool-row.e2e.ts
index af6c941bcd..3e18ff9548 100644
--- a/apps/web/tests/skill-tool-row.e2e.ts
+++ b/apps/web/tests/skill-tool-row.e2e.ts
@@ -17,7 +17,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/skill-tool-row', import.
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/skill-tool-row/ui.expected.md', import.meta.url))
const MODE = webSnapshotMode()
const SEED_ID = 'skill-tool-row-web-e2e'
-const PROMPT = 'Load the snapshot-skill skill with the skill tool, then reply DONE.'
+const PROMPT = 'Load the editing-cordis-compositions skill with the skill tool, then reply DONE.'
describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
let scaffold: WebScaffold
@@ -53,17 +53,17 @@ describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
it('expands the loaded skill to its exact recorded instructions', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-skill-tool-row'))
const call = page.locator('[data-tool="skill"]')
- const row = call.getByRole('button', { name: 'Skill snapshot-skill' })
+ const row = call.getByRole('button', { name: 'Skill editing-cordis-compositions' })
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('false')
- expect(await call.getByText('snapshot-skill', { exact: true }).count()).toBe(1)
+ expect(await call.getByText('editing-cordis-compositions', { exact: true }).count()).toBe(1)
await row.click()
await expect.poll(() => row.getAttribute('aria-expanded')).toBe('true')
await call.getByText('Instructions', { exact: true }).waitFor()
const output = call.locator('pre')
await output.waitFor()
- expect(await output.textContent()).toContain('')
- expect(await output.textContent()).toContain('Follow these snapshot-only instructions.')
+ expect(await output.textContent()).toContain('')
+ expect(await output.textContent()).toContain('The Claude Code Bundle installs and exclusively uses the matching platform CLI')
expect(await output.evaluate(element => getComputedStyle(element.parentElement!).maxHeight)).toBe('260px')
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
diff --git a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
index 6dfa55d454..fe2eecebad 100644
--- a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
+++ b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
@@ -1,13 +1,13 @@
- banner:
- navigation "Session hierarchy":
- - button "Load the snapshot-skill skill with" [disabled]
+ - button "Load the editing-cordis-compositions ski" [disabled]
- button "Session log":
- text: Session log
- img
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
-- text: Load the snapshot-skill skill with the skill tool, then reply DONE. {{date}} {{clock}}
+- text: Load the editing-cordis-compositions skill with the skill tool, then reply DONE. {{date}} {{clock}}
- button "Copy":
- img
- button "Context injection @deepseek-ai/dsh-system-prompt":
@@ -22,10 +22,10 @@
- img
- img
- text: Think Load the requested skill.
-- button "Skill snapshot-skill" [expanded]:
+- button "Skill editing-cordis-compositions" [expanded]:
- img
- - text: Skill snapshot-skill
-- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/snapshot-skill Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. Follow these snapshot-only instructions. Resolve referenced resources relative to this skill directory. "
+ - text: Skill editing-cordis-compositions
+- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created. ## The roster service `ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. Read `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on: - `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. - `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). ```js return { name: 'preset-tools', inject: ['agentPresets', 'tools'], apply(ctx) { harness.registerTool(ctx, harness.defineTool({ name: 'preset_check', description: 'Mount-validate one preset by id.', parameters: { id: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, async execute(args) { try { await ctx.agentPresets.standingKeyFor(args.id) return 'mounted OK' } catch (error) { return error.message } }, })) }, } ``` Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset 1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. ## The rule that catches people **A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: ```yaml - id: delegation name: cordis:group group: true isolate: workflows: true config: - id: workflow-worker-thread name: '@deepseek-ai/dsh-workflow-worker-thread' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` `true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. ## Verifying a change **`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); - a row that never activated (`N row(s) did not activate: : waiting for `); - a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. **Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. `cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. `cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. ## Native product subagents 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 add @deepseek-ai/dsh-subagent-codex dsh plugin --profile 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 remove ` 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: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex enableRunInBackground: false maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code enableRunInBackground: false maxDepth: provider-managed ``` 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 provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. "
- button "Inspect"
- button "Think The skill is loaded.":
- img
diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
index 0f9608ae4c..7292e114d4 100644
--- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
@@ -19,7 +19,7 @@
{"type":"assistant/chunk","seq":17,"time":1785730426828,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":18,"time":1785730426828,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fd7a47e-84c9-4d31-aa95-9939671ba0a5"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[10,11,12,13,14,15,16,17],"surfaceOp":"append"}
{"type":"tool/call","seq":19,"time":1785730426828,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}
-{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nCodex 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:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe 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 remove ` withdraws only that provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n enableRunInBackground: false\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false\n maxDepth: provider-managed\n```\n\nThe 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 starts, authenticates, selects a model for, probes, or manages a host-level installation of either product. The Claude Code Bundle's Agent SDK dependency still carries its platform CLI payload, which production ignores in favor of the host's `claude`.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"fa340fc0-3edc-4a61-92b2-2c4d70c4b6d7"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
+{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nCodex 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:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe 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 remove ` withdraws only that provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n enableRunInBackground: false\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false\n maxDepth: provider-managed\n```\n\nThe 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 provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"fa340fc0-3edc-4a61-92b2-2c4d70c4b6d7"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","seq":21,"time":1785730426838,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":22,"time":1785730426848,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index cb854f43ed..e93da05c24 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/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: b1a5c4bb4b9d1c3221c38092d8b0b967888b7746
-README.zh.md: b17b78f03f47b35409e211d814dce444835058bd
+README.md: 058e72a46cd6fb7c15779f3650e191535a8b2571
+README.zh.md: 05914165c26162de9c0a37cfa7090a8d667e63bc
diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md
index b1a5c4bb4b..058e72a46c 100644
--- a/packages/subagent/subagent-claude-code/README.md
+++ b/packages/subagent/subagent-claude-code/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
+This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, lets the pinned SDK select its installed platform CLI, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
## Start and ownership
@@ -29,9 +29,9 @@ The provider advertises no optional start-time capabilities and reports `inherit
| `env` | `{}` | Explicit SDK/CLI environment layered over the shared credential-scrubbed parent environment. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
-Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
+Production omits `pathToClaudeCodeExecutable`, so Agent SDK 0.3.220 selects the matching native `claude` or `claude.exe` from its own platform package and passes that absolute command through the custom-spawn hook to `dsh-subprocess`. The provider does not inspect `PATH`, implement platform selection, or fall back to a host `claude`. Native settings and authentication remain authoritative. The plugin does not select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden; `PATH` does not choose the Claude executable.
-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.
+This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; installation brings the pinned Agent SDK and one compatible platform CLI payload into that Profile, while the declared `cordis.patch.yml` layer registers only the dormant `claude-code` Host provider and starts no Claude process. Removing the package withdraws that provider and its private runtime closure on the next Profile start.
```sh
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
@@ -63,7 +63,9 @@ Installation controls Host availability, not model permission. Full Agent Preset
## Product compatibility and evidence
-The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`. Production runs the native `claude` installation. The keyless real-product test uses the SDK-distributed Claude Code 2.1.220 CLI as a deterministic fixture, routed through the same native executable-resolution and Windows batch-shim path; it does not claim compatibility with every independently installed version. Loader composition proves that both product packages coexist without starting either product.
+The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose eight platform packages carry Claude Code 2.1.220. A normal install selects one payload for the current OS, CPU, and Linux libc. For the current darwin-arm64 payload, `npm pack --dry-run --json` reports 74,858,812 packed bytes and 256,908,856 unpacked bytes; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test runs the SDK-selected CLI against a loopback Messages fixture and asserts that the shared subprocess argv begins with that platform package's native executable. Loader composition proves that both product packages coexist without starting either product.
+
+Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload leaves provider registration dormant but makes the first delegation fail with the SDK's native-payload startup error. The provider neither probes a host CLI nor retries with one.
The project owner's identity-scoped distribution authorization covers the official SDK and the official CLI/platform payloads declared by each SDK version. [`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) discloses the current optional payload closure without classifying its declared terms as permissive; unrelated non-permissive runtime dependencies continue to fail the notices gate.
@@ -73,7 +75,7 @@ The project owner's identity-scoped distribution authorization covers the offici
#### What the model sees
-The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd, while its model, system instructions, tools, permissions, and authentication come from the host's native Claude settings and product installation.
+The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd, while its model, system instructions, tools, permissions, and authentication come from native Claude settings; the executable version comes from the Bundle's pinned SDK platform payload.
#### Token effect
@@ -101,8 +103,8 @@ Append-only: the new tool result follows the reusable parent request prefix.
- **One fresh query and process per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
- **Host settings are intentionally authoritative** — project and user settings can change model, tools, and behavior; the provider does not provide a filtered or hermetic production mode.
-- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
-- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
+- **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, or rewrite Claude settings; configuration and authentication failures surface as startup or run errors.
+- **The SDK platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first query; there is no host-CLI fallback.
- **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending.
- **Final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index b17b78f03f..05914165c2 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/packages/subagent/subagent-claude-code/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-本包(package)注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
+本包(package)注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,让锁定版本的 SDK 选择随包安装的平台 CLI,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
## 启动与所有权
@@ -29,9 +29,9 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
-生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
+生产环境会省略 `pathToClaudeCodeExecutable`,因此 Agent SDK 0.3.220 会从自己的平台包中选择匹配的原生 `claude` 或 `claude.exe`,再通过 custom-spawn 钩子把该绝对命令交给 `dsh-subprocess`。提供方不会检查 `PATH`、重复实现平台选择,也不会回退到宿主 `claude`。原生设置与身份验证继续是权威来源。本插件不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承;`PATH` 不参与选择 Claude 可执行文件。
-本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;包所声明的 `cordis.patch.yml` 层只注册休眠的 `claude-code` Host provider,不会启动 Claude 进程。移除该包后,下一次 Profile 启动会撤回这一 provider。
+本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;安装会把锁定的 Agent SDK 与一个兼容的平台 CLI 载荷带入该 Profile,而包所声明的 `cordis.patch.yml` 层只注册休眠的 `claude-code` Host provider,不会启动 Claude 进程。移除该包后,下一次 Profile 启动会撤回这一 provider 及其私有运行时闭包。
```sh
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
@@ -63,7 +63,9 @@ dsh --profile
## 产品兼容性与证据
-运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装。无密钥真实产品测试使用由 SDK 分发的 Claude Code 2.1.220 CLI 作为确定性 fixture(测试前置数据),并通过同一套原生可执行文件解析路径与 Windows batch shim 路径运行;这项测试不声称兼容每个独立安装的版本。Loader 组合证明两个产品包能够共存且不会启动任一产品。
+运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其八个平台包都携带 Claude Code 2.1.220。普通安装会按当前操作系统、CPU 及 Linux libc 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json` 报告压缩包为 74,858,812 字节、解包后为 256,908,856 字节;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会让 SDK 选择 CLI,通过回环 Messages fixture 运行它,并断言共享子进程 argv 的首项就是该平台包的原生可执行文件。Loader 组合证明两个产品包能够共存且不会启动任一产品。
+
+如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,提供方注册仍保持休眠,但第一次委派会以 SDK 的原生载荷启动错误失败。提供方既不会探测宿主 CLI,也不会用它重试。
限定于项目所有者身份的分发授权涵盖官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会认定其中声明的条款属于宽松许可;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
@@ -73,7 +75,7 @@ dsh --profile
#### 模型看到的内容
-Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、权限和身份验证来自宿主机原生 Claude 设置与产品安装。
+Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、权限和身份验证来自原生 Claude 设置,可执行版本则来自 Bundle 锁定的 SDK 平台载荷。
#### 对 token 的影响
@@ -101,8 +103,8 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
- **每次运行均新建一个 query 和一个进程**:不支持续接、恢复、池化、进度流或产品会话持久化。
- **宿主设置有意保持权威**:项目和用户设置可以改变模型、工具与行为;本提供方不提供经过筛选或与宿主环境隔离的生产模式。
-- **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
-- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
+- **身份验证与账户状态仍由原生机制管理**:Bundle 会提供 CLI,但不会创建账户、登录或改写 Claude 设置;配置与身份验证失败会呈现为启动错误或运行错误。
+- **委派时必须存在 SDK 平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次 query 时失败;不会回退到宿主 CLI。
- **没有人工交互路径**:`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。
- **仅返回最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
diff --git a/packages/subagent/subagent-claude-code/src/index.ts b/packages/subagent/subagent-claude-code/src/index.ts
index ccd150b746..fef8a5141e 100644
--- a/packages/subagent/subagent-claude-code/src/index.ts
+++ b/packages/subagent/subagent-claude-code/src/index.ts
@@ -66,18 +66,12 @@ class ClaudeCodeProvider implements SubagentProvider {
'subagent-claude-code: no working directory for the child — delegate from a parent session that has one',
)
}
- const executable = await this.ctx.subprocess.resolveExecutable(
- 'claude',
- this.config.env,
- request.signal,
- )
const spec: ClaudeCodeRunSpec = {
cwd: resolveChildCwd(
'subagent-claude-code',
undefined,
parentCwd,
),
- executable,
env: this.config.env,
disposeGraceMs: this.config.disposeGraceMs,
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),
diff --git a/packages/subagent/subagent-claude-code/src/process.ts b/packages/subagent/subagent-claude-code/src/process.ts
index 1e2a259ca2..32a545bf08 100644
--- a/packages/subagent/subagent-claude-code/src/process.ts
+++ b/packages/subagent/subagent-claude-code/src/process.ts
@@ -6,7 +6,6 @@
*/
import { EventEmitter } from 'node:events'
-import { extname } from 'node:path'
import type {
SpawnedProcess,
SpawnOptions,
@@ -17,8 +16,6 @@ import {
type SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
-const WINDOWS_BATCH_EXECUTABLE_ENV = 'DSH_CLAUDE_CODE_EXECUTABLE'
-
function thrown(value: unknown): Error {
/* v8 ignore next -- the subprocess seam rejects with Error. */
return value instanceof Error ? value : new Error(String(value))
@@ -43,33 +40,22 @@ export function sdkEnvironmentOverlay(
* Translate one official SDK spawn request to the shared process owner.
* @param options - command, arguments, workspace, environment, and forwarded signal from the SDK.
* @param graceMs - process-tree termination grace.
- * @param platform - host platform selecting the Windows batch-shim boundary.
* @returns the fully explicit shared subprocess request.
- * @remarks The batch-shim path quotes only the resolved executable. The pinned SDK
- * supplies fixed flag arguments without cmd metacharacters; cmd reparses that tail.
*/
export function claudeSpawnSpec(
options: SpawnOptions,
graceMs: number,
- platform: NodeJS.Platform = process.platform,
): SubprocessSpawnSpec {
if (options.cwd === undefined || options.cwd.length === 0) {
throw new Error('subagent-claude-code: SDK spawn request omitted its workspace')
}
- const extension = extname(options.command).toLowerCase()
- const batchShim = platform === 'win32' && (extension === '.cmd' || extension === '.bat')
- const env = sdkEnvironmentOverlay(options.env)
- const argv = batchShim
- ? ['cmd.exe', '/d', '/v:off', '/s', '/c', `%${WINDOWS_BATCH_EXECUTABLE_ENV}%`, ...options.args]
- : [options.command, ...options.args]
- if (batchShim) env[WINDOWS_BATCH_EXECUTABLE_ENV] = `"${options.command}"`
return {
- argv,
+ argv: [options.command, ...options.args],
cwd: options.cwd,
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
graceMs,
signal: options.signal,
- env,
+ env: sdkEnvironmentOverlay(options.env),
}
}
diff --git a/packages/subagent/subagent-claude-code/src/run.ts b/packages/subagent/subagent-claude-code/src/run.ts
index 6c1e0a8dbf..9dc4d740ac 100644
--- a/packages/subagent/subagent-claude-code/src/run.ts
+++ b/packages/subagent/subagent-claude-code/src/run.ts
@@ -44,8 +44,6 @@ export const DEFAULT_DISPOSE_GRACE_MS = 3_000
export interface ClaudeCodeRunSpec {
/** Parent Session workspace supplied to the SDK and real CLI. */
readonly cwd: string
- /** Exact native Claude Code executable resolved from the host PATH. */
- readonly executable: string
/** Explicit deployment/test environment layered after shared scrubbing. */
readonly env: Record
/** Subprocess termination grace passed to the shared process-tree owner. */
@@ -182,7 +180,6 @@ export function claudeQueryOptions(
return {
abortController: controller,
cwd: spec.cwd,
- pathToClaudeCodeExecutable: spec.executable,
env: { ...scrubbedParentEnv(), ...spec.env },
persistSession: false,
disallowedTools: ['AskUserQuestion'],
diff --git a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts
index 89d08a1878..1881e14d33 100644
--- a/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts
+++ b/packages/subagent/subagent-claude-code/tests/real-deepseek.e2e.ts
@@ -7,7 +7,7 @@ import {
rmSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
-import { delimiter, dirname, join, resolve } from 'node:path'
+import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
@@ -87,7 +87,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
]) mkdirSync(directory)
const env = {
- PATH: `${dirname(claudeBin)}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',
diff --git a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts
index f6767817c8..768952887f 100644
--- a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts
@@ -3,12 +3,12 @@ import {
mkdirSync,
mkdtempSync,
readFileSync,
- symlinkSync,
+ realpathSync,
writeFileSync,
} from 'node:fs'
import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
-import { delimiter, dirname, join, resolve } from 'node:path'
+import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import type {
@@ -119,7 +119,6 @@ interface RealHarness {
readonly parent: Agent
readonly workspace: string
readonly env: Record
- readonly executable: string
}
async function realHarness(behavior: MessagesBehavior): Promise<{
@@ -131,17 +130,9 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const workspace = join(root, 'workspace')
const claudeConfig = join(root, 'claude-config')
const xdgConfig = join(root, 'xdg')
- const nativeBin = join(root, 'native&%literal%!bang!bin')
mkdirSync(workspace)
mkdirSync(claudeConfig)
mkdirSync(xdgConfig)
- mkdirSync(nativeBin)
- const executable = join(nativeBin, process.platform === 'win32' ? 'claude.cmd' : 'claude')
- if (process.platform === 'win32') {
- writeFileSync(executable, `@echo off\r\n"${claudeBin}" %*\r\n`)
- } else {
- symlinkSync(claudeBin, executable)
- }
writeFileSync(
join(claudeConfig, 'settings.json'),
`${JSON.stringify({ model: settingsModel }, null, 2)}\n`,
@@ -149,7 +140,6 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const fixture = await startMessagesFixture(behavior)
fixtures.push(fixture)
const env = {
- PATH: `${nativeBin}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_API_KEY: fakeKey,
ANTHROPIC_BASE_URL: fixture.baseUrl,
CLAUDE_CONFIG_DIR: claudeConfig,
@@ -183,7 +173,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
session: { header: { cwd: workspace } },
} as unknown as Agent
return {
- harness: { ctx, handles, spawnSpecs, parent, workspace, env, executable },
+ harness: { ctx, handles, spawnSpecs, parent, workspace, env },
fixture,
}
}
@@ -225,7 +215,7 @@ describe('real Claude Agent SDK 0.3.220 and its distributed Claude Code 2.1.220
expect(sdkPackage.version).toBe('0.3.220')
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
- const version = await execFileAsync(process.platform === 'win32' ? claudeBin : harness.executable, ['--version'], {
+ const version = await execFileAsync(claudeBin, ['--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
@@ -242,18 +232,16 @@ describe('real Claude Agent SDK 0.3.220 and its distributed Claude Code 2.1.220
message.type === 'system' && message.subtype === 'init',
)
expect(initMessage?.claude_code_version).toBe('2.1.220')
- if (process.platform === 'win32') {
- expect(harness.spawnSpecs[0]?.argv.slice(0, 6)).toEqual([
- 'cmd.exe', '/d', '/v:off', '/s', '/c', '%DSH_CLAUDE_CODE_EXECUTABLE%',
- ])
- const batchExecutable = harness.spawnSpecs[0]?.env?.DSH_CLAUDE_CODE_EXECUTABLE
- expect(batchExecutable?.startsWith('"')).toBe(true)
- expect(batchExecutable?.endsWith('"')).toBe(true)
- expect(batchExecutable?.slice(1, -1).toLowerCase())
- .toBe(harness.executable.toLowerCase())
- } else {
- expect(harness.spawnSpecs[0]?.argv[0]).toBe(harness.executable)
- }
+ const spawnedExecutable = harness.spawnSpecs[0]?.argv[0]
+ expect(spawnedExecutable).toBeDefined()
+ expect(process.platform === 'win32'
+ ? realpathSync(spawnedExecutable!).toLowerCase()
+ : realpathSync(spawnedExecutable!))
+ .toBe(process.platform === 'win32'
+ ? realpathSync(claudeBin).toLowerCase()
+ : realpathSync(claudeBin))
+ expect(harness.spawnSpecs[0]?.env)
+ .not.toHaveProperty('DSH_CLAUDE_CODE_EXECUTABLE')
expect(fixture.requests).toHaveLength(1)
const recorded = fixture.requests[0]!
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index 0fb0e55a98..9d1bd7c6bc 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
@@ -1,5 +1,5 @@
import { readFileSync } from 'node:fs'
-import { resolve } from 'node:path'
+import { dirname, resolve } from 'node:path'
import { PassThrough } from 'node:stream'
import { fileURLToPath } from 'node:url'
import type {
@@ -56,6 +56,19 @@ type QueryFactory = (params: {
const queryMock = vi.hoisted(() => vi.fn())
+const CLAUDE_AGENT_SDK_VERSION = '0.3.220'
+const CLAUDE_CODE_VERSION = '2.1.220'
+const CLAUDE_PLATFORM_PACKAGES = [
+ '@anthropic-ai/claude-agent-sdk-darwin-arm64',
+ '@anthropic-ai/claude-agent-sdk-darwin-x64',
+ '@anthropic-ai/claude-agent-sdk-linux-arm64',
+ '@anthropic-ai/claude-agent-sdk-linux-arm64-musl',
+ '@anthropic-ai/claude-agent-sdk-linux-x64',
+ '@anthropic-ai/claude-agent-sdk-linux-x64-musl',
+ '@anthropic-ai/claude-agent-sdk-win32-arm64',
+ '@anthropic-ai/claude-agent-sdk-win32-x64',
+] as const
+
vi.mock('@anthropic-ai/claude-agent-sdk', async importOriginal => ({
...await importOriginal(),
query: queryMock,
@@ -252,7 +265,6 @@ function fakeRun(
const options: FakeRun['options'] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
- executable: '/native/claude',
env: { ANTHROPIC_API_KEY: 'fake-key' },
disposeGraceMs: 5,
spawn: (spawnSpec) => {
@@ -295,9 +307,41 @@ describe('task admission and package contracts', () => {
}
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
expect(manifest.files).toContain('cordis.patch.yml')
- expect(manifest.dependencies).toHaveProperty('@anthropic-ai/claude-agent-sdk')
+ expect(manifest.dependencies).toHaveProperty(
+ '@anthropic-ai/claude-agent-sdk',
+ CLAUDE_AGENT_SDK_VERSION,
+ )
expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
+ const sdkRoot = dirname(fileURLToPath(
+ import.meta.resolve('@anthropic-ai/claude-agent-sdk'),
+ ))
+ const sdkManifest = JSON.parse(readFileSync(
+ resolve(sdkRoot, 'package.json'),
+ 'utf8',
+ )) as {
+ version: string
+ claudeCodeVersion: string
+ optionalDependencies: Record
+ }
+ expect(sdkManifest.version).toBe(CLAUDE_AGENT_SDK_VERSION)
+ expect(sdkManifest.claudeCodeVersion).toBe(CLAUDE_CODE_VERSION)
+ expect(sdkManifest.optionalDependencies).toEqual(Object.fromEntries(
+ CLAUDE_PLATFORM_PACKAGES.map(packageName => [
+ packageName,
+ CLAUDE_AGENT_SDK_VERSION,
+ ]),
+ ))
+ const lockfile = readFileSync(resolve(root, '../../../pnpm-lock.yaml'), 'utf8')
+ for (const packageName of CLAUDE_PLATFORM_PACKAGES) {
+ expect(lockfile).toContain(
+ ` '${packageName}@${CLAUDE_AGENT_SDK_VERSION}':`,
+ )
+ expect(lockfile).toContain(
+ ` '${packageName}': ${CLAUDE_AGENT_SDK_VERSION}`,
+ )
+ }
+
const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'))
const rows = Array.isArray(parsed)
? (parsed as Array<{ insert?: Array<{ id?: string; name?: string }> }>).flatMap(entry => entry.insert ?? [])
@@ -360,7 +404,7 @@ describe('task admission and package contracts', () => {
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
.mockImplementation(() => child.handle)
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable')
- .mockResolvedValue('/native/claude')
+ .mockResolvedValue('/host/bin/claude')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
await ctx.plugin(claudeCode, {
env: {
@@ -382,10 +426,15 @@ describe('task admission and package contracts', () => {
)
expect(queryMock).not.toHaveBeenCalled()
- resolveExecutable.mockRejectedValueOnce(new Error('claude missing from PATH'))
+ vi.stubEnv('PATH', '/host/bin')
+ queryMock.mockImplementationOnce(() => {
+ throw new Error(
+ 'Native CLI binary for fixture-platform not found. Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set options.pathToClaudeCodeExecutable.',
+ )
+ })
await expect(ctx.subagents.start('claude-code', request()))
- .rejects.toThrow('claude missing from PATH')
- expect(queryMock).not.toHaveBeenCalled()
+ .rejects.toThrow('Native CLI binary for fixture-platform not found')
+ expect(resolveExecutable).not.toHaveBeenCalled()
const run = await ctx.subagents.start('claude-code', request())
child.settle({ exitCode: 9, signal: null })
@@ -397,13 +446,9 @@ describe('task admission and package contracts', () => {
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
'subagent-claude-code: child run failed (error):',
))
- expect(resolveExecutable).toHaveBeenCalledWith(
- 'claude',
- expect.objectContaining({ ANTHROPIC_API_KEY: 'provider-fake-key' }),
- expect.any(AbortSignal),
- )
- expect(queryMock.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable)
- .toBe('/native/claude')
+ expect(resolveExecutable).not.toHaveBeenCalled()
+ expect(queryMock.mock.calls[1]?.[0].options)
+ .not.toHaveProperty('pathToClaudeCodeExecutable')
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
cwd: process.cwd(),
graceMs: 29,
@@ -483,20 +528,17 @@ describe('official spawn projection', () => {
)).toThrow('SDK spawn request omitted its workspace')
})
- it.each(['cmd', 'bat'])('routes a Windows .%s shim through cmd.exe', (extension) => {
- const command = String.raw`C:\Program Files\Claude\claude.${extension}`
+ it('forwards the SDK-selected Windows native executable without a batch shim', () => {
+ const command = String.raw`C:\Program Files\Claude\claude.exe`
const spec = claudeSpawnSpec(sdkSpawnOptions({
command,
args: ['--output-format', 'stream-json'],
- }), 7, 'win32')
+ }), 7)
expect(spec.argv).toEqual([
- 'cmd.exe', '/d', '/v:off', '/s', '/c', '%DSH_CLAUDE_CODE_EXECUTABLE%',
- '--output-format', 'stream-json',
+ command, '--output-format', 'stream-json',
])
- expect(spec.env).toEqual(expect.objectContaining({
- DSH_CLAUDE_CODE_EXECUTABLE: `"${command}"`,
- }))
+ expect(spec.env).not.toHaveProperty('DSH_CLAUDE_CODE_EXECUTABLE')
})
it('projects streams, exit facts, listeners, and idempotent tree termination', async () => {
@@ -566,7 +608,6 @@ describe('query options and result mapping', () => {
const captured: SubprocessHandle[] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
- executable: '/native/claude',
env: {
HOST_VISIBLE: 'overridden',
ANTHROPIC_API_KEY: 'explicit-fake-key',
@@ -582,10 +623,10 @@ describe('query options and result mapping', () => {
expect(options).toMatchObject({
abortController: controller,
cwd: '/workspace',
- pathToClaudeCodeExecutable: '/native/claude',
persistSession: false,
disallowedTools: ['AskUserQuestion'],
})
+ expect(options).not.toHaveProperty('pathToClaudeCodeExecutable')
expect(options.env).toMatchObject({
HOST_VISIBLE: 'overridden',
ANTHROPIC_API_KEY: 'explicit-fake-key',
@@ -730,7 +771,6 @@ describe('run publication, cancellation, and settlement', () => {
let index = 0
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
- executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => children[index++]!.handle,
@@ -781,7 +821,6 @@ describe('run publication, cancellation, and settlement', () => {
request(undefined, parentAbort.signal),
{
cwd: '/workspace',
- executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => child.handle,
From 8fa6ad9132561c66c3016a024a4b1a6c1d141db1 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Thu, 13 Aug 2026 19:34:56 +0800
Subject: [PATCH 006/134] fix(subagent): preserve Claude spawn diagnostics
---
.../subagent/subagent-claude-code/src/run.ts | 41 ++++++++++--
.../tests/subagent-claude-code.spec.ts | 63 ++++++++++++++++++-
2 files changed, 97 insertions(+), 7 deletions(-)
diff --git a/packages/subagent/subagent-claude-code/src/run.ts b/packages/subagent/subagent-claude-code/src/run.ts
index 9dc4d740ac..6b20f2f838 100644
--- a/packages/subagent/subagent-claude-code/src/run.ts
+++ b/packages/subagent/subagent-claude-code/src/run.ts
@@ -58,6 +58,11 @@ function thrown(value: unknown): Error {
/* v8 ignore next -- typed SDK and subprocess failures reject with Error. */
return value instanceof Error ? value : new Error(String(value))
}
+
+/** Read live request cancellation across awaited startup cleanup. */
+function isAborted(signal: AbortSignal): boolean {
+ return signal.aborted
+}
/* jscpd:ignore-end */
/**
@@ -236,12 +241,39 @@ export async function startClaudeCodeRun(
request.signal.removeEventListener('abort', onAbort)
const cancelledBeforeCleanup = controller.signal.aborted
requestCancel()
+ const startupError = thrown(error)
+ if (child !== undefined && child.pid <= 0) {
+ let closeError: Error | undefined
+ try {
+ query?.close()
+ } catch (disposeError: unknown) {
+ closeError = thrown(disposeError)
+ }
+
+ let spawnError = startupError
+ try {
+ await child.done
+ } catch (childError: unknown) {
+ spawnError = thrown(childError)
+ }
+
+ if (cancelledBeforeCleanup || isAborted(request.signal)) {
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
+ }
+ if (closeError !== undefined) {
+ throw new AggregateError(
+ [spawnError, closeError],
+ `subagent-claude-code: Claude Code process startup failed: ${spawnError.message}; query cleanup also failed`,
+ )
+ }
+ throw spawnError
+ }
if (child !== undefined) {
try {
await disposeClaudeCodeChild(query, child)
} catch (disposeError: unknown) {
throw new AggregateError(
- [thrown(error), thrown(disposeError)],
+ [startupError, thrown(disposeError)],
'subagent-claude-code: startup failed and CLI cleanup also failed',
)
}
@@ -250,16 +282,15 @@ export async function startClaudeCodeRun(
query.close()
} catch (disposeError: unknown) {
throw new AggregateError(
- [thrown(error), thrown(disposeError)],
+ [startupError, thrown(disposeError)],
'subagent-claude-code: startup failed and query cleanup also failed',
)
}
}
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- the request can abort while process cleanup is awaited.
- if (cancelledBeforeCleanup || request.signal.aborted) {
+ if (cancelledBeforeCleanup || isAborted(request.signal)) {
throw new Error('subagent-claude-code: request was aborted before SDK startup')
}
- throw thrown(error)
+ throw startupError
}
const publishedQuery = query
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index 9d1bd7c6bc..34d0fb150b 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
@@ -908,14 +908,73 @@ describe('run publication, cancellation, and settlement', () => {
expect(factoryController?.signal.aborted).toBe(true)
expect(spawned.terminate).toHaveBeenCalledOnce()
+ const spawnError = Object.assign(
+ new Error('spawn /sdk/claude EACCES'),
+ { code: 'EACCES', path: '/sdk/claude' },
+ )
const failedSpawn = fakeChild({
pid: -1,
- doneError: new Error('spawn failed'),
+ doneError: spawnError,
})
const failed = fakeRun([], undefined, failedSpawn)
await expect(startClaudeCodeRun(request(), failed.spec))
- .rejects.toBeInstanceOf(AggregateError)
+ .rejects.toBe(spawnError)
expect(failed.close).toHaveBeenCalledOnce()
+ expect(failedSpawn.terminate).not.toHaveBeenCalled()
+ expect(failedSpawn.waitForExit).not.toHaveBeenCalled()
+
+ const failedSpawnAbort = new AbortController()
+ const cancelledFailedSpawn = fakeChild({
+ pid: -1,
+ doneError: spawnError,
+ })
+ const cancelledFailedClose = vi.fn()
+ queryMock.mockImplementationOnce(({ options }) => {
+ options.spawnClaudeCodeProcess!(sdkSpawnOptions())
+ failedSpawnAbort.abort(new Error('startup cancelled'))
+ return queryFrom([], undefined, cancelledFailedClose)
+ })
+ await expect(startClaudeCodeRun(
+ request(undefined, failedSpawnAbort.signal),
+ { ...unused.spec, spawn: () => cancelledFailedSpawn.handle },
+ )).rejects.toThrow('aborted before SDK startup')
+ expect(cancelledFailedClose).toHaveBeenCalledOnce()
+
+ const failedSpawnCloseError = new Error('query close failed')
+ const failedSpawnClose = vi.fn(() => { throw failedSpawnCloseError })
+ const failedSpawnWithCloseFailure = fakeChild({
+ pid: -1,
+ doneError: spawnError,
+ })
+ queryMock.mockImplementationOnce(({ options }) => {
+ options.spawnClaudeCodeProcess!(sdkSpawnOptions())
+ return queryFrom([], undefined, failedSpawnClose)
+ })
+ const failedWithCloseFailure = startClaudeCodeRun(request(), {
+ ...unused.spec,
+ spawn: () => failedSpawnWithCloseFailure.handle,
+ })
+ await expect(failedWithCloseFailure)
+ .rejects.toThrow('spawn /sdk/claude EACCES')
+ await expect(failedWithCloseFailure).rejects.toMatchObject({
+ errors: [spawnError, failedSpawnCloseError],
+ })
+
+ const cleanupError = new Error('live child cleanup failed')
+ const constructionError = new Error(
+ 'query construction failed with a live child',
+ )
+ const liveChildCleanupFailure = fakeChild({ doneError: cleanupError })
+ queryMock.mockImplementationOnce(({ options }) => {
+ options.spawnClaudeCodeProcess!(sdkSpawnOptions())
+ throw constructionError
+ })
+ await expect(startClaudeCodeRun(request(), {
+ ...unused.spec,
+ spawn: () => liveChildCleanupFailure.handle,
+ })).rejects.toMatchObject({
+ errors: [constructionError, cleanupError],
+ })
})
})
From 9fcdb2c160ab34dd04d90c4fcf82601a0e77eac1 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Thu, 13 Aug 2026 20:30:36 +0800
Subject: [PATCH 007/134] fix(subagent): close Claude SDK runtime dependencies
---
apps/cli/reference/README.i18n.yaml | 4 ++--
apps/cli/reference/README.md | 2 +-
apps/cli/reference/README.zh.md | 2 +-
packages/subagent/subagent-claude-code/package.json | 4 +++-
.../subagent-claude-code/tests/subagent-claude-code.spec.ts | 5 +++++
pnpm-lock.yaml | 6 ++++++
6 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 38a333beea..95635b579d 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: 4bb0502a9af9299fed991bb8a2279d74ccf97037
-README.zh.md: 1b641cefaa4f5ece496fc6af2447e45cd308f395
+README.md: f1fc9a2857fb143e435a9aa3cddbddfd03b72ee2
+README.zh.md: 2d9036f80e602525405947beae8ee6c4cfcec645
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index 4bb0502a9a..f1fc9a2857 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile 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 starts no product process. The Codex provider resolves a host `codex` from `PATH`; the Claude Code Bundle instead installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native product settings remain user-managed for both products; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. 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 or its platform payloads.
+The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, each installed product Bundle registers only its dormant Host provider and starts no product process. The Codex provider resolves a host `codex` from `PATH`; the Claude Code Bundle instead installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native product settings remain user-managed for both products; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. 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 or its platform payloads.
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index 1b641cefaa..2d9036f80e 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -52,7 +52,7 @@ dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑仍保留既有热重载行为。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动产品进程。Codex provider 会从 `PATH` 解析宿主 `codex`;Claude Code Bundle 则会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。两个产品的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动产品进程。Codex provider 会从 `PATH` 解析宿主 `codex`;Claude Code Bundle 则会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。两个产品的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json
index 3399234904..3eb33f524f 100644
--- a/packages/subagent/subagent-claude-code/package.json
+++ b/packages/subagent/subagent-claude-code/package.json
@@ -49,7 +49,9 @@
"dependencies": {
"@anthropic-ai/sdk": "0.93.0",
"@anthropic-ai/claude-agent-sdk": "0.3.220",
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/schemastery": "workspace:^",
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index 34d0fb150b..bfbc117a70 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
@@ -311,6 +311,11 @@ describe('task admission and package contracts', () => {
'@anthropic-ai/claude-agent-sdk',
CLAUDE_AGENT_SDK_VERSION,
)
+ expect(manifest.dependencies).toHaveProperty(
+ '@modelcontextprotocol/sdk',
+ '^1.29.0',
+ )
+ expect(manifest.dependencies).toHaveProperty('zod', '^4.4.3')
expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex')
const sdkRoot = dirname(fileURLToPath(
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2850bbe5b5..64ce4f8242 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7074,6 +7074,12 @@ importers:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
+ '@modelcontextprotocol/sdk':
+ specifier: ^1.29.0
+ version: 1.29.0(zod@4.4.3)
+ zod:
+ specifier: ^4.4.3
+ version: 4.4.3
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
From b4366e711d634290101b6b82deafac405e3de3b1 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Fri, 14 Aug 2026 16:05:48 +0800
Subject: [PATCH 008/134] feat(subagent): make Claude Code provider directly
installable
---
...ubagent-providers-in-shared-host.i18n.yaml | 4 +-
...oduct-subagent-providers-in-shared-host.md | 14 ++---
...ct-subagent-providers-in-shared-host.zh.md | 14 ++---
...code-and-codex-subagent-backends.i18n.yaml | 4 +-
...claude-code-and-codex-subagent-backends.md | 6 +-
...ude-code-and-codex-subagent-backends.zh.md | 6 +-
...ludes-product-subagent-providers.i18n.yaml | 4 +-
...dsh-excludes-product-subagent-providers.md | 8 +--
...-excludes-product-subagent-providers.zh.md | 8 +--
.../agent-presets/code/agent.cordis.yml | 4 +-
.../agent-presets/cordis/agent.cordis.yml | 4 +-
.../editing-cordis-compositions/SKILL.md | 8 +--
.../agent-presets/standard/agent.cordis.yml | 4 +-
apps/cli/reference/README.i18n.yaml | 4 +-
apps/cli/reference/README.md | 7 +--
apps/cli/reference/README.zh.md | 7 +--
apps/cli/tests/web-agent-presets.e2e.ts | 57 ++++++-------------
.../snapshots/skill-tool-row/ui.expected.md | 2 +-
docs/module-graph.i18n.yaml | 4 +-
docs/module-graph.md | 15 ++---
docs/module-graph.zh.md | 15 ++---
.../subagent/subagent-codex/cordis.yml | 7 ++-
.../subagent/subagent-codex/driver.ts | 9 ++-
.../tests/snapshots/skill-load/session.jsonl | 2 +-
packages/bundle/README.i18n.yaml | 4 +-
packages/bundle/README.md | 2 +-
packages/bundle/README.zh.md | 2 +-
packages/bundle/base/README.i18n.yaml | 4 +-
packages/bundle/base/README.md | 2 +-
packages/bundle/base/README.zh.md | 4 +-
packages/subagent/README.i18n.yaml | 4 +-
packages/subagent/README.md | 2 +-
packages/subagent/README.zh.md | 2 +-
.../subagent-claude-code/README.i18n.yaml | 4 +-
.../subagent/subagent-claude-code/README.md | 2 +-
.../subagent-claude-code/README.zh.md | 2 +-
.../subagent/subagent-claude-code/src/run.ts | 20 +++++--
.../tests/subagent-claude-code.spec.ts | 28 +++++++++
.../subagent/subagent-codex/README.i18n.yaml | 4 +-
packages/subagent/subagent-codex/README.md | 15 +----
packages/subagent/subagent-codex/README.zh.md | 15 +----
.../subagent/subagent-codex/cordis.patch.yml | 6 --
packages/subagent/subagent-codex/package.json | 9 +--
.../tests/loader-composition.e2e.ts | 9 ---
.../tests/subagent-codex.spec.ts | 29 ----------
pnpm-lock.yaml | 6 +-
.../verify-config-source-ownership.spec.ts | 4 +-
scripts/verify-cordis-config.spec.ts | 7 +--
48 files changed, 171 insertions(+), 232 deletions(-)
delete mode 100644 packages/subagent/subagent-codex/cordis.patch.yml
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index e8572ed53d..b6bff51d29 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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: 2b1a417f7e751b2edee7e3b23dd439feab1353ea
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: 2f687b7dc9332680b8aa610f46668f197066c517
+2026-08-10-product-subagent-providers-in-shared-host.md: 564fd315c41c2f6999eed65bd7241e8b7167f43e
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: eaa24bb323191b14edbd2f756033b700374f5356
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index 2b1a417f7e..564fd315c4 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -6,21 +6,21 @@ 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) 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 [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) are separate packages loaded beside the common subagent tool. The Claude Code package is directly installable as a Profile Bundle, while a deployment mounts the Codex package explicitly. Agent Presets are the ordinary owner of one agent's model-visible tools, but a preset cannot safely own either provider: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Host availability and Preset tool grants 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 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
-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.
+The Claude Code Bundle and an explicit Codex Host row each load their fixed 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 is absent remains unavailable rather than mounting another provider in the Agent plane.
-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 [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, the Claude Code package owns a directly installable Bundle patch, and Codex remains an explicitly mounted Host plugin. This note continues to own process-wide Host placement whenever either provider is present. 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 have different executable owners. Codex starts a host `codex` from `PATH`. The Claude Code Bundle installs its pinned Agent SDK and matching platform CLI; the provider lets that SDK choose the private native executable and passes the command through the shared subprocess owner without consulting or falling back to a host `claude`. Loading either Bundle only registers the provider and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing Codex command or Claude platform payload, authentication failure, and other product failures remain local to the attempted delegation.
+The providers have different executable owners. Codex starts a host `codex` from `PATH`. The Claude Code Bundle installs its pinned Agent SDK and matching platform CLI; the provider lets that SDK choose the private native executable and passes the command through the shared subprocess owner without consulting or falling back to a host `claude`. Loading either provider only registers it and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing Codex command or Claude platform payload, authentication failure, and other product failures remain local to the attempted delegation.
## Verification
-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 host executable resolution for Codex, SDK platform-payload selection without fallback for Claude Code, failure, cancellation, and process-tree quiescence.
+Real composition loads either no Claude Code Bundle or the Claude Code Bundle and crosses that availability with Agent Presets that leave its tool disabled or grant it. It proves the Host registry and model-visible tools reflect those two decisions, no product process starts during composition, and Preset edits affect only later Sessions. Existing Codex Loader and provider tests separately prove explicit Host composition and host executable resolution. Keyless ACP snapshots pin the model-visible tool schemas, while provider tests prove SDK platform-payload selection without fallback for Claude Code, failure, cancellation, and process-tree quiescence.
## Alternatives considered
@@ -34,6 +34,6 @@ Real composition loads the selected set of no product Bundle, Codex only, Claude
## Consequences
-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.
+A user installs the Claude Code Bundle only in Profiles that need it, while a deployment that uses Codex mounts that Host plugin explicitly. Model-visible grants use the same Agent Preset authoring path as other plugins. Each new Session receives the intersection of its preset's tool rows and the Host's available providers. A present but ungranted product remains dormant and consumes its package and module-loading footprint but no product process, login, model call, or product home; an absent product contributes no provider closure.
-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.
+The Host registry remains the single provider authority, the Profile Bundle or explicit Host composition 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.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index 2f687b7dc9..eaa24bb323 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -6,21 +6,21 @@ Status: implemented
## 问题
-[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)是可独立安装的包,由部署环境在通用 subagent 工具旁加载。Agent Preset 是单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。因此,Bundle 安装与 Preset 工具授权分别属于部署决策和 agent 创作决策。
+[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)由两个独立包实现,并在通用 subagent 工具旁加载。Claude Code 包可作为 Profile Bundle 直接安装,而部署环境会显式挂载 Codex 包。Agent Preset 是单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有任一产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。因此,Host 可用性与 Preset 工具授权分别属于部署决策和 agent 创作决策。
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具授权仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
## 决策
-产品 Bundle 安装到 Profile 后,会在共享 Host 平面中恰好加载一次其固定的 `codex` 或 `claude-code` 提供方。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不授权任何工具、只授权其中一个或同时授权两者,而无需更改提供方注册表。若工具对应的提供方 Bundle 尚未安装,该工具仍不可用,而不会在 Agent 平面中另行挂载提供方。
+Claude Code Bundle 与显式 Codex Host 行都会在共享 Host 平面中恰好加载一次各自固定的提供方。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不授权任何工具、只授权其中一个或同时授权两者,而无需更改提供方注册表。若工具对应的提供方不存在,该工具仍不可用,而不会在 Agent 平面中另行挂载提供方。
-[生产依赖闭包决策](../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 创作,以及改动只影响新组装会话的规则。
+[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只部分取代本说明先前关于默认包含提供方的选择:base 组合包排除两个提供方,Claude Code 包拥有可直接安装的 Bundle patch,而 Codex 仍是显式挂载的 Host 插件。本说明继续负责任一提供方存在时的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
-两个提供方的可执行文件归属不同。Codex 会启动从 `PATH` 解析出的宿主 `codex`。Claude Code Bundle 会安装锁定的 Agent SDK 与匹配平台 CLI;提供方让 SDK 选择该私有原生可执行文件,再把命令交给共享子进程责任方,既不查询也不回退宿主 `claude`。加载任一 Bundle 只会注册提供方,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。Codex 命令缺失、Claude 平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
+两个提供方的可执行文件归属不同。Codex 会启动从 `PATH` 解析出的宿主 `codex`。Claude Code Bundle 会安装锁定的 Agent SDK 与匹配平台 CLI;提供方让 SDK 选择该私有原生可执行文件,再把命令交给共享子进程责任方,既不查询也不回退宿主 `claude`。加载任一提供方只会完成注册,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。Codex 命令缺失、Claude 平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
## 验证
-真实组装会加载未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装这四种集合,并与不授权工具、仅授权其中一个或同时授权两者的 Agent Preset 完整交叉。测试证明 Host 注册表等于已安装 Bundle 集合,模型可见工具等于已安装且已授权集合的交集,并且组装期间不会启动产品进程。Preset 编辑覆盖继续证明代际隔离。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则分别证明 Codex 的宿主可执行文件解析、Claude Code 的 SDK 平台载荷选择与无回退行为,以及失败、取消和进程树完全停稳。
+真实组装会覆盖未安装 Claude Code Bundle 与已安装该 Bundle 两种状态,并分别使用保留禁用行或授权该工具的 Agent Preset。测试证明 Host 注册表和模型可见工具会反映这两个决策,组装期间不会启动产品进程,而且 Preset 编辑只影响后续 Session。现有 Codex Loader 与提供方测试会另行证明显式 Host 组装和宿主可执行文件解析。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则证明 Claude Code 的 SDK 平台载荷选择与无回退行为,以及失败、取消和进程树完全停稳。
## 考虑过的替代方案
@@ -34,6 +34,6 @@ Status: implemented
## 后果
-用户只安装 Profile 可用的产品 Bundle,并通过与其他插件相同的 Agent Preset 创作路径管理模型可见授权。每个新会话会获得其 preset 工具行与 Profile 已安装提供方的交集。已安装但未授权的产品保持休眠,会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录;未安装的产品不会进入提供方或 SDK 依赖闭包。
+用户只在需要 Claude Code 的 Profile 中安装该 Bundle;使用 Codex 的部署会显式挂载对应 Host 插件。模型可见授权仍通过与其他插件相同的 Agent Preset 创作路径管理。每个新 Session 会获得其 preset 工具行与 Host 可用提供方的交集。存在但未授权的产品保持休眠,会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录;缺席的产品不会进入提供方闭包。
-Host 注册表仍是提供方的唯一权威,每个 Bundle 仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
+Host 注册表仍是提供方的唯一权威,Profile Bundle 或显式 Host 组装仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 856c199c8b..f5bb0b18c3 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.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: f9c7529db664d5b7bebcd77ba69fd974d6cad5b3
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 8b0e42507c5a0850d569bff3c9abe962b4a03fe0
+2026-08-04-claude-code-and-codex-subagent-backends.md: fed85f5d338db4b6e53406c94e3ecfd29aebd5af
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 2de5f72bc22bc56318209f13ca09746e6add58b2
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index f9c7529db6..fed85f5d33 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.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) 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.
+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, while the [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns both providers' exclusion from the default distribution and Claude Code's optional direct Bundle installation. Codex remains an explicitly mounted Host plugin. 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, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader and optional Bundle-composition evidence resolve the selected product packages by name while starting neither product.
+The Claude Code evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader coverage resolves Codex through explicit Host composition and Claude Code through its optional Bundle while starting neither product.
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
@@ -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. 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.
+Users delegate through two stable foreground tools backed by the official product integrations. Available 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); Claude Code Bundle availability and both providers' 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. Codex behavior depends on the deployment's installed CLI and native configuration; Claude Code behavior depends on the Bundle-pinned platform CLI plus native account 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.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index 8b0e42507c..2de5f72bc2 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.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)负责提供方安装后的进程级放置,[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责其可选直接 Bundle 安装与默认发行排除。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
+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)负责两个提供方的默认发行排除,以及 Claude Code 的可选直接 Bundle 安装。Codex 仍是显式挂载的 Host 插件。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。
这两个提供方都报告 `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、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 与可选 Bundle 组装证据会按名称解析已选择的产品包且不启动产品。
+Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 覆盖会通过显式 Host 组装解析 Codex,并通过可选 Bundle 解析 Claude Code,且不会启动任一产品。
带密钥 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、Claude Code 2.1.220,以及八
## 后果
-用户通过官方产品集成支持的两个稳定前台工具进行委派。已安装提供方位于进程级 Host、工具按 Preset 暴露,这些规则由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;可选包可用性与默认排除由[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
+用户通过官方产品集成支持的两个稳定前台工具进行委派。可用提供方位于进程级 Host、工具按 Preset 暴露,这些规则由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;Claude Code Bundle 可用性与两个提供方的默认排除由[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。
每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。Codex 行为取决于部署环境中安装的 CLI 与原生配置;Claude Code 行为取决于 Bundle 锁定的平台 CLI,以及原生账户和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
index b22e11f805..f842aecafe 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
+++ b/.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: b729115ead5ec6823b0c98815fa12f50022defdb
-2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: ac10ae2b4f04ddc3997b5fe4bbb8f656742de547
+2026-08-12-production-dsh-excludes-product-subagent-providers.md: 779bff6f668c086722f817a28a224182d0fbac09
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 27106668f8b81491d43e2033c1468799783c0584
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
index b729115ead..779bff6f66 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
@@ -10,13 +10,13 @@ English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers
## Decision
-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.
+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. The Claude Code provider package is 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 Codex package remains available for deployments that mount it explicitly.
-The two Bundles remain independent. The Codex Bundle owns its `@deepseek-ai/dsh-sdk-protocol` runtime dependency and continues to use a host `codex` from `PATH`. The Claude Code Bundle owns the pinned Agent SDK and the matching platform CLI selected from the SDK's optional dependencies; production uses that private CLI and never falls back to a host `claude`. Installing one Bundle does not pull in the other, and the default `@deepseek-ai/dsh` production closure contains neither provider, the Claude Agent SDK, nor its platform payloads. 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 brings only the selected package closure onto disk; it does not start a product, authenticate an account, rewrite native settings, or grant model access.
+The two optional integrations remain independent. Codex continues to use a host `codex` from `PATH`. The Claude Code Bundle owns the pinned Agent SDK and the matching platform CLI selected from the SDK's optional dependencies; production uses that private CLI and never falls back to a host `claude`. Installing the Claude Code Bundle does not pull in the Codex package, and the default `@deepseek-ai/dsh` production closure contains neither provider, the Claude Agent SDK, nor its platform payloads. The Bundle registers a dormant provider on the next Profile start, while an Agent Preset independently decides whether a new Session receives its tool. Installation brings only the Claude Code package closure onto disk; it does not start a product, authenticate an account, rewrite native settings, or grant model access.
## Verification
-Package tests pin each Bundle manifest, published patch, exact self-provider row, and product-specific runtime closure. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform package identities and versions, the SDK-selected executable entering the shared subprocess owner, and first-delegation failure without host fallback when the payload is missing. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. 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, the full tool-grant matrix on a Host with both providers, representative missing-provider cases, and zero product processes. The base bundle test continues to reject both provider dependencies and configuration rows.
+Package tests pin the Claude Code Bundle manifest, published patch, exact self-provider row, and runtime closure. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform package identities and versions, the SDK-selected executable entering the shared subprocess owner, and first-delegation failure without host fallback when the payload is missing. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Production-closure tests prove the default and Claude-only dependency boundaries, while real Bundle-patch and Agent-Preset composition covers absent and installed Host states, disabled and enabled tool grants, later-Session adoption, and zero product processes. Existing Codex package tests continue to cover explicit Host composition and host executable resolution. The base bundle test continues to reject both provider dependencies and configuration rows.
## Alternatives considered
@@ -26,4 +26,4 @@ Package tests pin each Bundle manifest, published patch, exact self-provider row
## Consequences
-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. Selecting Claude Code explicitly accepts its SDK and one large platform CLI payload, while selecting Codex does not install a product CLI. 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.
+Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. A Profile can add or remove the Claude Code provider Bundle directly; the changed Host availability takes effect on the next Profile start and explicitly accepts its SDK plus one large platform CLI payload. A Codex deployment still mounts that provider explicitly and supplies its product CLI through `PATH`. A separately authored Agent Preset grants either model-visible tool only to newly composed Sessions. No wrapper package, meta Bundle, dynamic installer, or persisted product-enable state is introduced.
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
index ac10ae2b4f..27106668f8 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
@@ -10,13 +10,13 @@ Status: implemented
## 决策
-本决策只部分取代[共享 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 工具行。
+本决策只部分取代[共享 host 放置决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)中关于默认包含提供方的部分:`@deepseek-ai/dsh-base` 不依赖也不挂载 Codex 与 Claude Code subagent 提供方。Claude Code 提供方包是可直接安装的 Profile Bundle,其 `dsh.bundle.patch` 指向包自身拥有的 `cordis.patch.yml`。该 patch 恰好贡献一条挂载自身提供方的 Host 行,不包含 Agent 工具行。Codex 包仍供部署环境显式挂载。
-两个 Bundle 彼此独立。Codex Bundle 自己负责运行时依赖 `@deepseek-ai/dsh-sdk-protocol`,并继续使用 `PATH` 中的宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK,以及从 SDK optional dependencies 中选出的匹配平台 CLI;生产运行只使用该私有 CLI,绝不会回退到宿主 `claude`。安装其中一个 Bundle 不会带入另一个,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含 Claude Agent SDK 或其平台载荷。已安装的 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装只会把所选包闭包放到磁盘上;它不会启动产品、验证账户、改写原生设置或向模型授予访问权。
+两个可选集成彼此独立。Codex 继续使用 `PATH` 中的宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK,以及从 SDK optional dependencies 中选出的匹配平台 CLI;生产运行只使用该私有 CLI,绝不会回退到宿主 `claude`。安装 Claude Code Bundle 不会带入 Codex 包,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含 Claude Agent SDK 或其平台载荷。该 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装只会把 Claude Code 包闭包放到磁盘上;它不会启动产品、验证账户、改写原生设置或向模型授予访问权。
## 验证
-包测试会固定每个 Bundle 的 manifest、发布 patch、准确的自身提供方行以及产品专属运行时闭包。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包的身份与版本、SDK 所选可执行文件进入共享子进程责任方的路径,以及载荷缺失时第一次委派失败且不回退宿主 CLI。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认、仅 Codex 与仅 Claude 三种依赖边界;真实 Bundle patch 与 Agent Preset 的组装会覆盖四种安装集合、同时安装两个提供方时的完整工具授权矩阵、缺失提供方的代表场景以及零产品进程。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
+包测试会固定 Claude Code Bundle 的 manifest、发布 patch、准确的自身提供方行以及运行时闭包。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包的身份与版本、SDK 所选可执行文件进入共享子进程责任方的路径,以及载荷缺失时第一次委派失败且不回退宿主 CLI。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认与仅 Claude 两种依赖边界;真实 Bundle patch 与 Agent Preset 组装会覆盖 Host 中缺席和已安装两种状态、禁用和启用两种工具授权、后续 Session 采纳以及零产品进程。现有 Codex 包测试继续覆盖显式 Host 组装和宿主可执行文件解析。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
## 考虑过的替代方案
@@ -26,4 +26,4 @@ Status: implemented
## 后果
-安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除任一提供方包,也可以同时操作两者;Host 可用性的变化会在下次 Profile 启动时生效。选择 Claude Code 代表明确接受其 SDK 与一个大型平台 CLI 载荷,而选择 Codex 不会安装产品 CLI。单独创作的 Agent Preset 仍只会向新组装的 Session 授予模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。
+安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除 Claude Code provider Bundle;Host 可用性的变化会在下次 Profile 启动时生效,并代表明确接受其 SDK 与一个大型平台 CLI 载荷。Codex 部署仍须显式挂载该 provider,并通过 `PATH` 提供产品 CLI。单独创作的 Agent Preset 仍只会向新组装的 Session 授予任一模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。
diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml
index ab6e42450e..a1643edbb2 100644
--- a/apps/cli/config/agent-presets/code/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/code/agent.cordis.yml
@@ -201,8 +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.
+ # Install the optional Claude Code Provider Bundle in this Profile and restart
+ # the Host before enabling its template. Installation alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
index 23766c18de..2fac142f83 100644
--- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
@@ -188,8 +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.
+ # Install the optional Claude Code Provider Bundle in this Profile and restart
+ # the Host before enabling its template. Installation alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index dcf2194352..916958e1c0 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
@@ -123,14 +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 are optional Profile Bundles. Install only the products the Profile needs, then restart the Profile so its Host registers those providers:
+The Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider:
```sh
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @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 remove ` withdraws only that provider on the next Profile start.
+The Bundle owns Claude Code 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 the package withdraws the 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:
@@ -154,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 an installed matching provider, and enabling both exposes the installed intersection. The host must provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings.
+The two rows are independent. The Claude Code row becomes available only when its Bundle is installed; the Codex row still requires a deployment whose Host composition registers that provider and whose `PATH` supplies `codex`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.
## What not to move into a preset
diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml
index c3e11aee19..c1721d0132 100644
--- a/apps/cli/config/agent-presets/standard/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml
@@ -200,8 +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.
+ # Install the optional Claude Code Provider Bundle in this Profile and restart
+ # the Host before enabling its template. Installation alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 95635b579d..0a5792a21c 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: f1fc9a2857fb143e435a9aa3cddbddfd03b72ee2
-README.zh.md: 2d9036f80e602525405947beae8ee6c4cfcec645
+README.md: c6c3f61c2910b84b9364ef7eaa13b8c0c011999b
+README.zh.md: d6f3e7f1f508724e54e59df01e93199193ddc506
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index f1fc9a2857..c6c3f61c29 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -42,17 +42,14 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` 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:
+The Claude Code subagent provider is an optional Bundle. Add or remove it independently:
```sh
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
-dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, each installed product Bundle registers only its dormant Host provider and starts no product process. The Codex provider resolves a host `codex` from `PATH`; the Claude Code Bundle instead installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native product settings remain user-managed for both products; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. 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 or its platform payloads.
+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 the Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, the installed package registers only its dormant Host provider and starts no Claude process. The Bundle installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native Claude settings remain user-managed; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the Claude Code row before a new Agent can see that tool. The Codex provider remains an explicitly mounted Host plugin that resolves `codex` from `PATH`; the default dsh dependency closure includes neither provider nor the Claude Agent SDK or its platform payloads.
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index 2d9036f80e..d6f3e7f1f5 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -42,17 +42,14 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `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。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
+Claude Code subagent provider 是一个可选 Bundle,可以独立添加或移除:
```sh
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
-dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装的产品 Bundle 只注册自己的休眠 Host provider,不会启动产品进程。Codex provider 会从 `PATH` 解析宿主 `codex`;Claude Code Bundle 则会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。两个产品的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用对应行,新 Agent 才能看到该工具。只安装一个 provider 不会安装另一个产品包;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,已安装的包只注册休眠的 Host provider,不会启动 Claude 进程。该 Bundle 会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。Claude 的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用 Claude Code 行,新 Agent 才能看到该工具。Codex provider 仍须作为 Host 插件显式挂载,并从 `PATH` 解析 `codex`;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 77185270e9..6c01becf76 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -26,7 +26,6 @@ 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')
@@ -434,12 +433,11 @@ describe('the shipped Web composition', () => {
})
})
-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'
+describe('Claude Code Bundle and user-preset intersection', () => {
+ const presetIds = ['products-none', 'products-claude'] as const
type PresetId = typeof presetIds[number]
- async function bootProducts(installed: readonly Product[]): Promise {
+ async function bootProducts(installed: boolean): Promise {
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
const userRoot = join(root, 'presets')
const settingsFile = join(root, 'settings.yaml')
@@ -447,20 +445,14 @@ describe('product subagent Bundle and user-preset intersection', () => {
await writeFile(settingsFile, '{}\n')
for (const id of presetIds) {
let composition = standard
- if (id === 'products-codex' || id === 'products-both') {
- composition = enablePresetTool(composition, 'tool-subagent-codex')
- }
- if (id === 'products-claude' || id === 'products-both') {
+ if (id === 'products-claude') {
composition = enablePresetTool(composition, 'tool-subagent-claude-code')
}
const directory = join(userRoot, id)
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'agent.cordis.yml'), composition)
}
- const productPatches = installed.flatMap(product => loadOverlayPatches(
- 'dsh-test',
- product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH,
- ))
+ const productPatches = installed ? loadOverlayPatches('dsh-test', CLAUDE_CODE_PATCH) : []
return await bootWeb(settingsFile, [
...productPatches,
{
@@ -474,21 +466,13 @@ describe('product subagent Bundle and user-preset intersection', () => {
includeUserRoot: false,
},
},
- ], installed.map(product => dirname(product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH)))
+ ], installed ? [dirname(CLAUDE_CODE_PATCH)] : [])
}
- it('composes the intersection of installed Bundles and enabled preset rows', async () => {
- const enabledByPreset: Record = {
- 'products-none': [],
- 'products-codex': ['codex'],
- 'products-claude': ['claude-code'],
- 'products-both': ['codex', 'claude-code'],
- }
- const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
- { installed: [], presets: ['products-both'] },
- { installed: ['codex'], presets: ['products-both'] },
- { installed: ['claude-code'], presets: ['products-both'] },
- { installed: ['codex', 'claude-code'], presets: presetIds },
+ it('composes the intersection of the installed Bundle and enabled preset row', async () => {
+ const scenarios: Array<{ installed: boolean; presets: readonly PresetId[] }> = [
+ { installed: false, presets: presetIds },
+ { installed: true, presets: presetIds },
]
for (const { installed, presets } of scenarios) {
@@ -498,21 +482,16 @@ describe('product subagent Bundle and user-preset intersection', () => {
expect(productCtx.subagents.list()
.filter(name => name === 'codex' || name === 'claude-code')
.sort())
- .toEqual([...installed].sort())
+ .toEqual(installed ? ['claude-code'] : [])
for (const id of presets) {
- const enabled = enabledByPreset[id]
const handle = await productCtx.agents.create({
- sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
+ sessionId: SessionId(`preset-${id}-${installed ? 'claude' : '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)
+ .toEqual(installed && id === 'products-claude' ? ['subagent_claude_code'] : [])
} finally {
await handle.dispose()
}
@@ -526,7 +505,7 @@ describe('product subagent Bundle and user-preset intersection', () => {
}, 120_000)
it('applies a product-row edit only to later sessions on the preset', async () => {
- const productCtx = await bootProducts(['codex'])
+ const productCtx = await bootProducts(true)
const preset = await productCtx.agentPresets.resolve('products-none')
const original = await readFile(preset.path, 'utf8')
const existing = await productCtx.agents.create({
@@ -534,16 +513,16 @@ describe('product subagent Bundle and user-preset intersection', () => {
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
})
try {
- expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
- await writeFile(preset.path, enablePresetTool(original, 'tool-subagent-codex'))
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_claude_code')
+ await writeFile(preset.path, enablePresetTool(original, 'tool-subagent-claude-code'))
const later = await productCtx.agents.create({
sessionId: SessionId('preset-product-generation-later'),
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
})
try {
- expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
- expect(toolNames(productCtx, later.agent)).toContain('subagent_codex')
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_claude_code')
+ expect(toolNames(productCtx, later.agent)).toContain('subagent_claude_code')
} finally {
await later.dispose()
}
diff --git a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
index fe2eecebad..e3f7e3f638 100644
--- a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
+++ b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
@@ -25,7 +25,7 @@
- button "Skill editing-cordis-compositions" [expanded]:
- img
- text: Skill editing-cordis-compositions
-- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created. ## The roster service `ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. Read `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on: - `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. - `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). ```js return { name: 'preset-tools', inject: ['agentPresets', 'tools'], apply(ctx) { harness.registerTool(ctx, harness.defineTool({ name: 'preset_check', description: 'Mount-validate one preset by id.', parameters: { id: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, async execute(args) { try { await ctx.agentPresets.standingKeyFor(args.id) return 'mounted OK' } catch (error) { return error.message } }, })) }, } ``` Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset 1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. ## The rule that catches people **A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: ```yaml - id: delegation name: cordis:group group: true isolate: workflows: true config: - id: workflow-worker-thread name: '@deepseek-ai/dsh-workflow-worker-thread' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` `true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. ## Verifying a change **`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); - a row that never activated (`N row(s) did not activate: : waiting for `); - a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. **Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. `cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. `cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. ## Native product subagents 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 add @deepseek-ai/dsh-subagent-codex dsh plugin --profile 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 remove ` 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: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex enableRunInBackground: false maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code enableRunInBackground: false maxDepth: provider-managed ``` 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 provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. "
+- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created. ## The roster service `ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. Read `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on: - `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. - `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). ```js return { name: 'preset-tools', inject: ['agentPresets', 'tools'], apply(ctx) { harness.registerTool(ctx, harness.defineTool({ name: 'preset_check', description: 'Mount-validate one preset by id.', parameters: { id: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, async execute(args) { try { await ctx.agentPresets.standingKeyFor(args.id) return 'mounted OK' } catch (error) { return error.message } }, })) }, } ``` Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset 1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. ## The rule that catches people **A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: ```yaml - id: delegation name: cordis:group group: true isolate: workflows: true config: - id: workflow-worker-thread name: '@deepseek-ai/dsh-workflow-worker-thread' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` `true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. ## Verifying a change **`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); - a row that never activated (`N row(s) did not activate: : waiting for `); - a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. **Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. `cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. `cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. ## Native product subagents The Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider: ```sh dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code ``` The Bundle owns Claude Code 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 the package withdraws the 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: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex enableRunInBackground: false maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code enableRunInBackground: false maxDepth: provider-managed ``` The two rows are independent. The Claude Code row becomes available only when its Bundle is installed; the Codex row still requires a deployment whose Host composition registers that provider and whose `PATH` supplies `codex`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. "
- button "Inspect"
- button "Think The skill is loaded.":
- img
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index 2e9e666180..353df8d744 100644
--- a/docs/module-graph.i18n.yaml
+++ b/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: 46f924b0df80aa667d4929e6d2707c8cf051bfb2
-module-graph.zh.md: 3a226bbdacfd1a5b850a406bea66bf29a5db209b
+module-graph.md: 24ee9a4815b4236336ae37f6c926dba4718dafb9
+module-graph.zh.md: d87feb946a43a2f9d391e7894b4a143af4e18406
diff --git a/docs/module-graph.md b/docs/module-graph.md
index 46f924b0df..24ee9a4815 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -961,12 +961,6 @@ flowchart TD
pkg_subagent_claude_code --> pkg_subagent
pkg_subagent_claude_code --> pkg_subprocess
pkg_subagent_claude_code --> pkg_timeout
- pkg_subagent_codex --> pkg_invariants
- pkg_subagent_codex --> pkg_llm
- pkg_subagent_codex --> pkg_session
- pkg_subagent_codex --> pkg_subagent
- pkg_subagent_codex --> pkg_subprocess
- pkg_subagent_codex --> pkg_timeout
pkg_subagent_in_process_driver --> pkg_agent
pkg_subagent_in_process_driver --> pkg_invariants
pkg_subagent_in_process_driver --> pkg_llm
@@ -1074,6 +1068,13 @@ flowchart TD
pkg_workflow_worker_thread --> pkg_subagent
pkg_workflow_worker_thread --> pkg_tools
pkg_workflow_worker_thread --> pkg_workflow
+ pkg_subagent_codex --> pkg_invariants
+ pkg_subagent_codex --> pkg_llm
+ pkg_subagent_codex --> pkg_sdk_protocol
+ pkg_subagent_codex --> pkg_session
+ pkg_subagent_codex --> pkg_subagent
+ pkg_subagent_codex --> pkg_subprocess
+ pkg_subagent_codex --> pkg_timeout
pkg_subagent_fork_in_process --> pkg_agent
pkg_subagent_fork_in_process --> pkg_invariants
pkg_subagent_fork_in_process --> pkg_session
@@ -1576,7 +1577,6 @@ flowchart TD
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1592,6 +1592,7 @@ flowchart TD
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index 3a226bbdac..d87feb946a 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -963,12 +963,6 @@ flowchart TD
pkg_subagent_claude_code --> pkg_subagent
pkg_subagent_claude_code --> pkg_subprocess
pkg_subagent_claude_code --> pkg_timeout
- pkg_subagent_codex --> pkg_invariants
- pkg_subagent_codex --> pkg_llm
- pkg_subagent_codex --> pkg_session
- pkg_subagent_codex --> pkg_subagent
- pkg_subagent_codex --> pkg_subprocess
- pkg_subagent_codex --> pkg_timeout
pkg_subagent_in_process_driver --> pkg_agent
pkg_subagent_in_process_driver --> pkg_invariants
pkg_subagent_in_process_driver --> pkg_llm
@@ -1076,6 +1070,13 @@ flowchart TD
pkg_workflow_worker_thread --> pkg_subagent
pkg_workflow_worker_thread --> pkg_tools
pkg_workflow_worker_thread --> pkg_workflow
+ pkg_subagent_codex --> pkg_invariants
+ pkg_subagent_codex --> pkg_llm
+ pkg_subagent_codex --> pkg_sdk_protocol
+ pkg_subagent_codex --> pkg_session
+ pkg_subagent_codex --> pkg_subagent
+ pkg_subagent_codex --> pkg_subprocess
+ pkg_subagent_codex --> pkg_timeout
pkg_subagent_fork_in_process --> pkg_agent
pkg_subagent_fork_in_process --> pkg_invariants
pkg_subagent_fork_in_process --> pkg_session
@@ -1578,7 +1579,6 @@ flowchart TD
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1594,6 +1594,7 @@ flowchart TD
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
index 45b601aed7..e770b11c95 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
@@ -1,5 +1,5 @@
-# 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.
+# 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.
- id: fixture
name: './fixture.ts'
@@ -9,6 +9,9 @@
- 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:
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
index af54c5fc0c..873f5e36de 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
@@ -1,21 +1,20 @@
#!/usr/bin/env node
/** Inspect the public Codex provider composition without invoking the product. */
-import { boot, loadOverlayPatches, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
+import { boot, 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]
-const bundlePatchPath = process.argv[3]
-if (configPath === undefined || bundlePatchPath === undefined) {
- throw new Error('subagent-codex Loader composition driver requires config and Bundle patch paths')
+if (configPath === undefined) {
+ throw new Error('subagent-codex Loader composition driver requires a config path')
}
let starts = 0
const ctx = await boot(
'subagent-codex-loader-composition',
resolveConfigPath(configPath, undefined),
- loadOverlayPatches('subagent-codex-loader-composition', bundlePatchPath),
+ undefined,
(hostCtx) => {
hostCtx.on('subagent/start', () => {
starts += 1
diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
index 7292e114d4..8e67021318 100644
--- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
@@ -19,7 +19,7 @@
{"type":"assistant/chunk","seq":17,"time":1785730426828,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":18,"time":1785730426828,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fd7a47e-84c9-4d31-aa95-9939671ba0a5"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[10,11,12,13,14,15,16,17],"surfaceOp":"append"}
{"type":"tool/call","seq":19,"time":1785730426828,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}
-{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nCodex 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:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe 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 remove ` withdraws only that provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n enableRunInBackground: false\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false\n maxDepth: provider-managed\n```\n\nThe 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 provide `codex` on `PATH` for the Codex provider. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither Bundle nor the preset starts a product during composition, authenticates an account, selects a model, probes credentials, or manages native product settings.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"fa340fc0-3edc-4a61-92b2-2c4d70c4b6d7"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
+{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nThe Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\ndsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe Bundle owns Claude Code 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 the package withdraws the provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n enableRunInBackground: false\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false\n maxDepth: provider-managed\n```\n\nThe two rows are independent. The Claude Code row becomes available only when its Bundle is installed; the Codex row still requires a deployment whose Host composition registers that provider and whose `PATH` supplies `codex`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"487d970e-9e85-4637-8b56-788e8cc00362"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","seq":21,"time":1785730426838,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":22,"time":1785730426848,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml
index 0441ec7d87..b42a5e5d52 100644
--- a/packages/bundle/README.i18n.yaml
+++ b/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: 4d7a064939ae04f25737b324ec35332b7b944f80
-README.zh.md: 8910b33a97acd2ef3ee5b659305739246004de01
+README.md: 3afa53c1444b43c38b7a71f3e9e00d271078be54
+README.zh.md: 740b3579ce1555f2b1b26ca79e8a3d915a338193
diff --git a/packages/bundle/README.md b/packages/bundle/README.md
index 4d7a064939..3afa53c144 100644
--- a/packages/bundle/README.md
+++ b/packages/bundle/README.md
@@ -4,7 +4,7 @@ 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.
+The manifest declaration, not this directory, defines Bundle identity. Domain packages can carry their own optional Profile layer; the [Claude Code subagent package](../subagent/subagent-claude-code/README.md) is a directly installable example.
| Package | Role | ctx key |
|---|---|---|
diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md
index 8910b33a97..740b3579ce 100644
--- a/packages/bundle/README.zh.md
+++ b/packages/bundle/README.zh.md
@@ -4,7 +4,7 @@
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)就是可直接安装的例子。
+Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包可以携带自己的可选 Profile 层;[Claude Code subagent 包](../subagent/subagent-claude-code/README.md)就是可直接安装的例子。
| 包 | 职责 | ctx key |
|---|---|---|
diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml
index cac587ee85..ea1fb9b03a 100644
--- a/packages/bundle/base/README.i18n.yaml
+++ b/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: a963bcca671c613ebdcc7b453384b1d9b8393662
-README.zh.md: ab43954fe3f4ad46160961e8ca67876df9aac503
+README.md: 5fdd642ecc03fea77b2b00fc6428525c1a40d891
+README.zh.md: c2a07ec15816e41eadf682bcd8631c93bce77ae0
diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md
index a963bcca67..5fdd642ecc 100644
--- a/packages/bundle/base/README.md
+++ b/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 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 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 can install the [Claude Code provider Bundle](../../subagent/subagent-claude-code/README.md) only when needed, while a deployment that uses Codex still mounts that provider explicitly. 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.
diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md
index ab43954fe3..c2a07ec158 100644
--- a/packages/bundle/base/README.zh.md
+++ b/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、遥测与核心 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,绝不通过代码。
+以 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 可以仅在需要时安装 [Claude Code provider Bundle](../../subagent/subagent-claude-code/README.md),使用 Codex 的部署仍须显式挂载该 provider。因此,默认的 `@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` 服务,配方不完整会在加载时直接报错)。POSIX 主机看到的是被禁用的 pwsh 行。
@@ -19,4 +19,4 @@ patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash`
## 已知限制与暂缓事项
- **patch 会替换整行 `config`**:profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。
-- **Windows 的临时目录授权是按会话的私有子目录**——`workspace-write` 把写入限制在工作区与会话自己的 temp 子目录(`\dsh-`,受限子进程的 TMP/TEMP 被改写);`read-only` 不授予任何写入。见 `@deepseek-ai/dsh-sandbox-windows-acl`。
+- **Windows 的临时目录授权是按会话的私有子目录**——`workspace-write` 把写入限制在工作区与会话自己的 temp 子目录(`\dsh-`,受限子进程的 TMP/TEMP 被改写);`read-only` 不授予任何临时目录写入权限。见 `@deepseek-ai/dsh-sandbox-windows-acl`。
diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml
index 6f83153195..24fefb0441 100644
--- a/packages/subagent/README.i18n.yaml
+++ b/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: 997d05c5030cc4e1ee9ebc1e8b4da2e6056b6266
-README.zh.md: a0e3ff4e17c1e0e092d56dc518bb16f0329c0198
+README.md: aebf368b984dca3ac2e37d1bcdba26a82ce85196
+README.zh.md: fb9e223f916a8b07d3a4ae254fa61087de081a93
diff --git a/packages/subagent/README.md b/packages/subagent/README.md
index 997d05c503..aebf368b98 100644
--- a/packages/subagent/README.md
+++ b/packages/subagent/README.md
@@ -18,7 +18,7 @@ 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 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.
+The Claude Code package is also an optional Profile Bundle. Install it with `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; the package registers only its dormant Host provider, while a copied Agent Preset separately grants the disabled tool template to new Sessions. Removing the package withdraws that provider on the next Profile start. The Codex package remains an explicitly mounted Host plugin and uses a host `codex` from `PATH`.
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).
diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md
index a0e3ff4e17..fb9e223f91 100644
--- a/packages/subagent/README.zh.md
+++ b/packages/subagent/README.zh.md
@@ -18,7 +18,7 @@
| [`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 add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code` 安装其中一个或两个包,再重启该 Profile;每个已安装包只注册自己的休眠 Host provider。完整 Agent Preset 仍保留彼此独立且默认禁用的工具模板,因此只安装 Bundle 不会向模型暴露工具。移除其中一个包后,下一次 Profile 启动只会撤回对应 provider。
+Claude Code 包也是一个可选的 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code` 安装后重启该 Profile;该包只注册休眠的 Host provider,而复制出的 Agent Preset 会单独把默认禁用的工具模板授予新 Session。移除该包后,下一次 Profile 启动会撤回对应 provider。Codex 包仍须作为 Host 插件显式挂载,并使用 `PATH` 中的宿主 `codex`。
参见有关[能力家族](../../.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)的决策。
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index e93da05c24..6d1e544c1b 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/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: 058e72a46cd6fb7c15779f3650e191535a8b2571
-README.zh.md: 05914165c26162de9c0a37cfa7090a8d667e63bc
+README.md: 226270506d743a7bb6023e27ae52c7e32b49c848
+README.zh.md: 453d1f15004267137f58969820e2de8d08ae943a
diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md
index 058e72a46c..226270506d 100644
--- a/packages/subagent/subagent-claude-code/README.md
+++ b/packages/subagent/subagent-claude-code/README.md
@@ -63,7 +63,7 @@ Installation controls Host availability, not model permission. Full Agent Preset
## Product compatibility and evidence
-The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose eight platform packages carry Claude Code 2.1.220. A normal install selects one payload for the current OS, CPU, and Linux libc. For the current darwin-arm64 payload, `npm pack --dry-run --json` reports 74,858,812 packed bytes and 256,908,856 unpacked bytes; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test runs the SDK-selected CLI against a loopback Messages fixture and asserts that the shared subprocess argv begins with that platform package's native executable. Loader composition proves that both product packages coexist without starting either product.
+The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose eight platform packages carry Claude Code 2.1.220. A normal install selects one payload for the current OS, CPU, and Linux libc. For the current darwin-arm64 payload, `npm pack --dry-run --json` reports 74,858,812 packed bytes and 256,908,856 unpacked bytes; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test runs the SDK-selected CLI against a loopback Messages fixture and asserts that the shared subprocess argv begins with that platform package's native executable. Loader composition proves that installing the Bundle registers only the dormant Claude Code provider and starts no product process.
Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload leaves provider registration dormant but makes the first delegation fail with the SDK's native-payload startup error. The provider neither probes a host CLI nor retries with one.
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index 05914165c2..453d1f1500 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/packages/subagent/subagent-claude-code/README.zh.md
@@ -63,7 +63,7 @@ dsh --profile
## 产品兼容性与证据
-运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其八个平台包都携带 Claude Code 2.1.220。普通安装会按当前操作系统、CPU 及 Linux libc 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json` 报告压缩包为 74,858,812 字节、解包后为 256,908,856 字节;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会让 SDK 选择 CLI,通过回环 Messages fixture 运行它,并断言共享子进程 argv 的首项就是该平台包的原生可执行文件。Loader 组合证明两个产品包能够共存且不会启动任一产品。
+运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其八个平台包都携带 Claude Code 2.1.220。普通安装会按当前操作系统、CPU 及 Linux libc 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json` 报告压缩包为 74,858,812 字节、解包后为 256,908,856 字节;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会让 SDK 选择 CLI,通过回环 Messages fixture 运行它,并断言共享子进程 argv 的首项就是该平台包的原生可执行文件。Loader 组合证明安装该 Bundle 只会注册休眠的 Claude Code provider,不会启动产品进程。
如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,提供方注册仍保持休眠,但第一次委派会以 SDK 的原生载荷启动错误失败。提供方既不会探测宿主 CLI,也不会用它重试。
diff --git a/packages/subagent/subagent-claude-code/src/run.ts b/packages/subagent/subagent-claude-code/src/run.ts
index 6b20f2f838..fbeca1ab9a 100644
--- a/packages/subagent/subagent-claude-code/src/run.ts
+++ b/packages/subagent/subagent-claude-code/src/run.ts
@@ -257,15 +257,25 @@ export async function startClaudeCodeRun(
spawnError = thrown(childError)
}
- if (cancelledBeforeCleanup || isAborted(request.signal)) {
- throw new Error('subagent-claude-code: request was aborted before SDK startup')
- }
+ const cancelled = cancelledBeforeCleanup || isAborted(request.signal)
if (closeError !== undefined) {
+ const failures = cancelled
+ ? [
+ new Error('subagent-claude-code: request was aborted before SDK startup'),
+ spawnError,
+ closeError,
+ ]
+ : [spawnError, closeError]
throw new AggregateError(
- [spawnError, closeError],
- `subagent-claude-code: Claude Code process startup failed: ${spawnError.message}; query cleanup also failed`,
+ failures,
+ cancelled
+ ? `subagent-claude-code: request was aborted before SDK startup; Claude Code process startup also failed: ${spawnError.message}; query cleanup also failed`
+ : `subagent-claude-code: Claude Code process startup failed: ${spawnError.message}; query cleanup also failed`,
)
}
+ if (cancelled) {
+ throw new Error('subagent-claude-code: request was aborted before SDK startup')
+ }
throw spawnError
}
if (child !== undefined) {
diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
index bfbc117a70..521fdf2d2d 100644
--- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
+++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts
@@ -945,6 +945,34 @@ describe('run publication, cancellation, and settlement', () => {
)).rejects.toThrow('aborted before SDK startup')
expect(cancelledFailedClose).toHaveBeenCalledOnce()
+ const cancelledFailedSpawnCloseError = new Error('cancelled query close failed')
+ const cancelledFailedSpawnClose = vi.fn(() => {
+ throw cancelledFailedSpawnCloseError
+ })
+ const cancelledFailedSpawnWithCloseFailure = fakeChild({
+ pid: -1,
+ doneError: spawnError,
+ })
+ const failedSpawnAbortWithCloseFailure = new AbortController()
+ queryMock.mockImplementationOnce(({ options }) => {
+ options.spawnClaudeCodeProcess!(sdkSpawnOptions())
+ failedSpawnAbortWithCloseFailure.abort(new Error('startup cancelled'))
+ return queryFrom([], undefined, cancelledFailedSpawnClose)
+ })
+ const cancelledWithCloseFailure = startClaudeCodeRun(
+ request(undefined, failedSpawnAbortWithCloseFailure.signal),
+ { ...unused.spec, spawn: () => cancelledFailedSpawnWithCloseFailure.handle },
+ )
+ await expect(cancelledWithCloseFailure).rejects.toMatchObject({
+ message: 'subagent-claude-code: request was aborted before SDK startup; Claude Code process startup also failed: spawn /sdk/claude EACCES; query cleanup also failed',
+ errors: [
+ expect.objectContaining({ message: 'subagent-claude-code: request was aborted before SDK startup' }),
+ spawnError,
+ cancelledFailedSpawnCloseError,
+ ],
+ })
+ expect(cancelledFailedSpawnClose).toHaveBeenCalledOnce()
+
const failedSpawnCloseError = new Error('query close failed')
const failedSpawnClose = vi.fn(() => { throw failedSpawnCloseError })
const failedSpawnWithCloseFailure = fakeChild({
diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml
index 2711220b5e..6f111f4ffc 100644
--- a/packages/subagent/subagent-codex/README.i18n.yaml
+++ b/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: 18e805f0e0a1d8d33ba77182ed73d213beb62e07
-README.zh.md: 7e376de43092b25c4206ee5e1cf5d736c2f1c910
+README.md: 3d59ca1eaf3db9dd9d9d2cd451692ebd2a956ef4
+README.zh.md: abff7b569e2ce8261366c004ab6d03ea53300fdb
diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md
index 18e805f0e0..3d59ca1eaf 100644
--- a/packages/subagent/subagent-codex/README.md
+++ b/packages/subagent/subagent-codex/README.md
@@ -27,26 +27,15 @@ 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.
-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 add @deepseek-ai/dsh-subagent-codex
-dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
-dsh --profile
-```
-
-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.
+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.
```yaml
-# $DSH_HOME/profiles//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
diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md
index 7e376de430..abff7b569e 100644
--- a/packages/subagent/subagent-codex/README.zh.md
+++ b/packages/subagent/subagent-codex/README.zh.md
@@ -27,26 +27,15 @@
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
-本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;包所声明的 `cordis.patch.yml` 层只注册休眠的 `codex` Host provider,不会启动 Codex 进程。移除该包后,下一次 Profile 启动会撤回这一 provider。
-
-```sh
-dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
-dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
-dsh --profile
-```
-
-安装决定 Host 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_codex`。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
+随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。自定义宿主组装仍可直接使用两条配置行。
```yaml
-# $DSH_HOME/profiles//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
diff --git a/packages/subagent/subagent-codex/cordis.patch.yml b/packages/subagent/subagent-codex/cordis.patch.yml
deleted file mode 100644
index 75e7464574..0000000000
--- a/packages/subagent/subagent-codex/cordis.patch.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-# 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'
diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json
index aa05afe260..0256ee8e21 100644
--- a/packages/subagent/subagent-codex/package.json
+++ b/packages/subagent/subagent-codex/package.json
@@ -28,18 +28,13 @@
"files": [
"lib/index.js",
"lib/invariant.js",
- "cordis.patch.yml",
"lib/types/**/*.d.ts"
],
"license": "MIT",
- "dsh": {
- "bundle": {
- "patch": "./cordis.patch.yml"
- }
- },
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
+ "@deepseek-ai/dsh-sdk-protocol": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
@@ -47,7 +42,6 @@
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
- "@deepseek-ai/dsh-sdk-protocol": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
@@ -56,6 +50,7 @@
"@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:^",
diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
index afdec98305..6c4019f8c8 100644
--- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
+++ b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
@@ -1,4 +1,3 @@
-import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
@@ -13,13 +12,6 @@ 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', () => {
@@ -30,7 +22,6 @@ 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.
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 4fa001a9f4..37b2e9ff0b 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -1,10 +1,6 @@
-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'
@@ -264,31 +260,6 @@ 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
- peerDependencies?: Record
- files?: string[]
- dsh?: { bundle?: { patch?: string } }
- }
- expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
- expect(manifest.files).toContain('cordis.patch.yml')
- expect(manifest.dependencies).toHaveProperty('@deepseek-ai/dsh-sdk-protocol')
- 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',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 64ce4f8242..0b13703008 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7114,9 +7114,6 @@ 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
@@ -7139,6 +7136,9 @@ importers:
'@deepseek-ai/dsh-loader-smoke':
specifier: workspace:^
version: link:../../test-support/loader-smoke
+ '@deepseek-ai/dsh-sdk-protocol':
+ specifier: workspace:^
+ version: link:../../sdk/protocol
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
diff --git a/scripts/verify-config-source-ownership.spec.ts b/scripts/verify-config-source-ownership.spec.ts
index 0c675c4f1a..3a099156c2 100644
--- a/scripts/verify-config-source-ownership.spec.ts
+++ b/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/subagent/subagent-codex')
+ const directory = join(root, 'packages/subagent/subagent-claude-code')
mkdirSync(directory, { recursive: true })
writeFileSync(
join(directory, 'cordis.patch.yml'),
@@ -22,7 +22,7 @@ describe('configuration source ownership gate', () => {
)
expect(collectConfigSourceOwnershipViolations(root)).toEqual([
- 'packages/subagent/subagent-codex/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
+ 'packages/subagent/subagent-claude-code/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
+ ' environment snapshot; inlining here bypasses both ladders.',
])
diff --git a/scripts/verify-cordis-config.spec.ts b/scripts/verify-cordis-config.spec.ts
index e889209516..07f655823b 100644
--- a/scripts/verify-cordis-config.spec.ts
+++ b/scripts/verify-cordis-config.spec.ts
@@ -117,17 +117,12 @@ describe('workspace Bundle discovery and product dependency closures', () => {
])
})
- it('keeps the default and two optional product closures independent', () => {
+ it('keeps the default and optional Claude Code closure 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')
From 15612c19986dd17eb5e8957751b377a864ef59dc Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Fri, 14 Aug 2026 16:40:23 +0800
Subject: [PATCH 009/134] review fix: trim duplicate closure evidence
---
apps/cli/reference/README.i18n.yaml | 4 +--
apps/cli/reference/README.md | 2 +-
apps/cli/reference/README.zh.md | 2 +-
scripts/verify-cordis-config.spec.ts | 44 +---------------------------
4 files changed, 5 insertions(+), 47 deletions(-)
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 0a5792a21c..cb2090c1f2 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: c6c3f61c2910b84b9364ef7eaa13b8c0c011999b
-README.zh.md: d6f3e7f1f508724e54e59df01e93199193ddc506
+README.md: f95973c05401d73a70db07b6ea4c76cd16f406f3
+README.zh.md: aa8a877bc53e430c867d384a2b0653255db5079d
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index c6c3f61c29..f95973c054 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -49,7 +49,7 @@ dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
dsh plugin --profile 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 the Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, the installed package registers only its dormant Host provider and starts no Claude process. The Bundle installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native Claude settings remain user-managed; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the Claude Code row before a new Agent can see that tool. The Codex provider remains an explicitly mounted Host plugin that resolves `codex` from `PATH`; the default dsh dependency closure includes neither provider nor the Claude Agent SDK or its platform payloads.
+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 the Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, the Bundle registers its dormant Host provider; a copied Preset must separately enable the matching tool row for new Agents. The [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) owns executable, authentication, payload, and failure details; the [subagent package reference](../../../packages/subagent/README.md) owns the current Codex deployment path; and the [base Bundle reference](../../../packages/bundle/base/README.md) owns the default dependency closure.
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index d6f3e7f1f5..aa8a877bc5 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -49,7 +49,7 @@ dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,已安装的包只注册休眠的 Host provider,不会启动 Claude 进程。该 Bundle 会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。Claude 的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用 Claude Code 行,新 Agent 才能看到该工具。Codex provider 仍须作为 Host 插件显式挂载,并从 `PATH` 解析 `codex`;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,Bundle 会注册休眠的 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)负责可执行文件、身份验证、载荷与失败细节;[subagent 包参考](../../../packages/subagent/README.md)负责当前 Codex 部署路径;[base Bundle 参考](../../../packages/bundle/base/README.md)负责默认依赖闭包。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/scripts/verify-cordis-config.spec.ts b/scripts/verify-cordis-config.spec.ts
index 07f655823b..f63031e46c 100644
--- a/scripts/verify-cordis-config.spec.ts
+++ b/scripts/verify-cordis-config.spec.ts
@@ -4,10 +4,9 @@
* metadata field must stay static, and a disabled expression must parse.
*/
-import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { mkdirSync, mkdtempSync, 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 {
bundleManifestPaths,
@@ -15,36 +14,6 @@ import {
metadataExpressionErrors,
} from './verify-cordis-config.ts'
-interface WorkspaceManifest {
- name?: string
- dependencies?: Record
- optionalDependencies?: Record
- peerDependencies?: Record
-}
-
-const repoRoot = fileURLToPath(new URL('..', import.meta.url))
-
-function productionClosure(entry: string): Set {
- const manifests = new Map()
- 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()
- 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', () => {
const problems = metadataExpressionErrors(
@@ -116,15 +85,4 @@ describe('workspace Bundle discovery and product dependency closures', () => {
`${file}: @deepseek-ai/dsh-missing-plugin must be declared in ${manifestPath} dependencies`,
])
})
-
- it('keeps the default and optional Claude Code closure 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 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')
- })
})
From b2178ade8028597e7565db77dfe3ef4ecd68b99d Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 03:25:43 +0800
Subject: [PATCH 010/134] feat(subagent): make Codex provider directly
installable
---
...ubagent-providers-in-shared-host.i18n.yaml | 4 +-
...oduct-subagent-providers-in-shared-host.md | 14 +-
...ct-subagent-providers-in-shared-host.zh.md | 14 +-
...code-and-codex-subagent-backends.i18n.yaml | 4 +-
...claude-code-and-codex-subagent-backends.md | 10 +-
...ude-code-and-codex-subagent-backends.zh.md | 10 +-
...ludes-product-subagent-providers.i18n.yaml | 4 +-
...dsh-excludes-product-subagent-providers.md | 10 +-
...-excludes-product-subagent-providers.zh.md | 10 +-
THIRD_PARTY_NOTICES.md | 18 ++-
.../agent-presets/code/agent.cordis.yml | 8 +-
.../agent-presets/cordis/agent.cordis.yml | 8 +-
.../editing-cordis-compositions/SKILL.md | 10 +-
.../agent-presets/standard/agent.cordis.yml | 8 +-
apps/cli/reference/README.i18n.yaml | 4 +-
apps/cli/reference/README.md | 7 +-
apps/cli/reference/README.zh.md | 7 +-
apps/cli/tests/web-agent-presets.e2e.ts | 59 ++++++---
apps/web/tests/skill-tool-row.e2e.ts | 2 +-
.../snapshots/skill-tool-row/ui.expected.md | 2 +-
docs/module-graph.i18n.yaml | 4 +-
docs/module-graph.md | 15 +--
docs/module-graph.zh.md | 15 +--
.../subagent/subagent-codex/cordis.yml | 7 +-
.../subagent/subagent-codex/driver.ts | 9 +-
.../tests/snapshots/skill-load/session.jsonl | 2 +-
packages/bundle/README.i18n.yaml | 4 +-
packages/bundle/README.md | 2 +-
packages/bundle/README.zh.md | 2 +-
packages/bundle/base/README.i18n.yaml | 4 +-
packages/bundle/base/README.md | 2 +-
packages/bundle/base/README.zh.md | 2 +-
packages/subagent/README.i18n.yaml | 4 +-
packages/subagent/README.md | 2 +-
packages/subagent/README.zh.md | 2 +-
.../subagent/subagent-codex/README.i18n.yaml | 4 +-
packages/subagent/subagent-codex/README.md | 35 +++--
packages/subagent/subagent-codex/README.zh.md | 35 +++--
.../subagent/subagent-codex/cordis.patch.yml | 5 +
packages/subagent/subagent-codex/package.json | 12 +-
packages/subagent/subagent-codex/src/index.ts | 4 +-
packages/subagent/subagent-codex/src/run.ts | 54 ++++++--
.../tests/loader-composition.e2e.ts | 9 ++
.../subagent-codex/tests/real-deepseek.e2e.ts | 8 +-
.../subagent-codex/tests/real-product.spec.ts | 41 +++++-
.../tests/subagent-codex.spec.ts | 91 +++++++++++--
pnpm-lock.yaml | 12 +-
scripts/gen-third-party-notices.spec.ts | 49 +++++++
scripts/gen-third-party-notices.ts | 121 +++++++++++++++++-
scripts/verify-cordis-config.spec.ts | 44 +------
50 files changed, 571 insertions(+), 242 deletions(-)
create mode 100644 packages/subagent/subagent-codex/cordis.patch.yml
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index b6bff51d29..14fc409ebc 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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: 564fd315c41c2f6999eed65bd7241e8b7167f43e
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: eaa24bb323191b14edbd2f756033b700374f5356
+2026-08-10-product-subagent-providers-in-shared-host.md: f1eac30e2984b6b9c1a0e83594a8f48dde690811
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: 14b7afcf414b9c058670d99531385da49d91fa4e
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index 564fd315c4..f1eac30e29 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -6,21 +6,21 @@ 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) are separate packages loaded beside the common subagent tool. The Claude Code package is directly installable as a Profile Bundle, while a deployment mounts the Codex package explicitly. Agent Presets are the ordinary owner of one agent's model-visible tools, but a preset cannot safely own either provider: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Host availability and Preset tool grants are therefore separate deployment and agent-authoring decisions.
+The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) are separate, directly installable Profile Bundle 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 either provider: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Bundle installation and Preset tool grants 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 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
-The Claude Code Bundle and an explicit Codex Host row each load their fixed 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 is absent remains unavailable rather than mounting another provider in the Agent plane.
+Each product Bundle loads its fixed 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 absent remains unavailable rather than mounting another provider in the Agent plane.
-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, the Claude Code package owns a directly installable Bundle patch, and Codex remains an explicitly mounted Host plugin. This note continues to own process-wide Host placement whenever either provider is present. 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 [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 either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
-The providers have different executable owners. Codex starts a host `codex` from `PATH`. The Claude Code Bundle installs its pinned Agent SDK and matching platform CLI; the provider lets that SDK choose the private native executable and passes the command through the shared subprocess owner without consulting or falling back to a host `claude`. Loading either provider only registers it and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing Codex command or Claude platform payload, authentication failure, and other product failures remain local to the attempted delegation.
+The Bundles have different executable owners. The Codex package pins the official wrapper and six platform aliases; the provider runs the package-declared wrapper, which selects the private native payload. The Claude Code package pins its Agent SDK and eight platform packages; the provider lets that SDK select the private native executable. Neither provider consults or falls back to a host product command, while native configuration and authentication remain authoritative. Loading either Bundle only registers the provider and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing platform payload, authentication failure, and other product failures remain local to the attempted delegation.
## Verification
-Real composition loads either no Claude Code Bundle or the Claude Code Bundle and crosses that availability with Agent Presets that leave its tool disabled or grant it. It proves the Host registry and model-visible tools reflect those two decisions, no product process starts during composition, and Preset edits affect only later Sessions. Existing Codex Loader and provider tests separately prove explicit Host composition and host executable resolution. Keyless ACP snapshots pin the model-visible tool schemas, while provider tests prove SDK platform-payload selection without fallback for Claude Code, failure, cancellation, and process-tree quiescence.
+Real composition loads no product Bundle, Codex only, Claude Code only, or both, then crosses that availability with Agent Presets that grant neither tool, either one, or both. It proves the Host registry and model-visible tools reflect those independent decisions, no product process starts during composition, and Preset edits affect only later Sessions. Package Loader and real-product tests separately prove each private runtime, missing-payload failure without host fallback, cancellation, and process-tree quiescence. Keyless ACP snapshots pin the model-visible tool schemas and generic Job controls.
## Alternatives considered
@@ -34,6 +34,6 @@ Real composition loads either no Claude Code Bundle or the Claude Code Bundle an
## Consequences
-A user installs the Claude Code Bundle only in Profiles that need it, while a deployment that uses Codex mounts that Host plugin explicitly. Model-visible grants use the same Agent Preset authoring path as other plugins. Each new Session receives the intersection of its preset's tool rows and the Host's available providers. A present but ungranted product remains dormant and consumes its package and module-loading footprint but no product process, login, model call, or product home; an absent product contributes no provider closure.
+A user installs only the product Bundles a Profile needs 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 Host'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 product-runtime closure.
-The Host registry remains the single provider authority, the Profile Bundle or explicit Host composition 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.
+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.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index eaa24bb323..14b7afcf41 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -6,21 +6,21 @@ Status: implemented
## 问题
-[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)由两个独立包实现,并在通用 subagent 工具旁加载。Claude Code 包可作为 Profile Bundle 直接安装,而部署环境会显式挂载 Codex 包。Agent Preset 是单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有任一产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。因此,Host 可用性与 Preset 工具授权分别属于部署决策和 agent 创作决策。
+[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)由两个可直接安装的独立 Profile Bundle 包实现,并在通用 subagent 工具旁加载。Agent Preset 是单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有任一产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。因此,Bundle 安装与 Preset 工具授权分别属于部署决策和 agent 创作决策。
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具授权仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
## 决策
-Claude Code Bundle 与显式 Codex Host 行都会在共享 Host 平面中恰好加载一次各自固定的提供方。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不授权任何工具、只授权其中一个或同时授权两者,而无需更改提供方注册表。若工具对应的提供方不存在,该工具仍不可用,而不会在 Agent 平面中另行挂载提供方。
+每个产品 Bundle 都会在共享 Host 平面中恰好加载一次各自固定的提供方。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不授权任何工具、只授权其中一个或同时授权两者,而无需更改提供方注册表。若工具对应的提供方 Bundle 未安装,该工具仍不可用,而不会在 Agent 平面中另行挂载提供方。
-[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只部分取代本说明先前关于默认包含提供方的选择:base 组合包排除两个提供方,Claude Code 包拥有可直接安装的 Bundle patch,而 Codex 仍是显式挂载的 Host 插件。本说明继续负责任一提供方存在时的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[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。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
-两个提供方的可执行文件归属不同。Codex 会启动从 `PATH` 解析出的宿主 `codex`。Claude Code Bundle 会安装锁定的 Agent SDK 与匹配平台 CLI;提供方让 SDK 选择该私有原生可执行文件,再把命令交给共享子进程责任方,既不查询也不回退宿主 `claude`。加载任一提供方只会完成注册,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。Codex 命令缺失、Claude 平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
+两个 Bundle 的可执行文件归属不同。Codex 包锁定官方 wrapper 与六个平台 alias;提供方运行包所声明的 wrapper,再由它选择私有原生载荷。Claude Code 包锁定 Agent SDK 与八个平台包;提供方让 SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令,原生配置与身份验证仍保持权威。加载任一 Bundle 只会完成提供方注册,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
## 验证
-真实组装会覆盖未安装 Claude Code Bundle 与已安装该 Bundle 两种状态,并分别使用保留禁用行或授权该工具的 Agent Preset。测试证明 Host 注册表和模型可见工具会反映这两个决策,组装期间不会启动产品进程,而且 Preset 编辑只影响后续 Session。现有 Codex Loader 与提供方测试会另行证明显式 Host 组装和宿主可执行文件解析。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema,提供方测试则证明 Claude Code 的 SDK 平台载荷选择与无回退行为,以及失败、取消和进程树完全停稳。
+真实组装会覆盖未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装四种状态,再与不授权工具、只授权其中一个或同时授权两者的 Agent Preset 交叉。测试证明 Host 注册表与模型可见工具会反映这两个独立决策,组装期间不会启动产品进程,而且 Preset 编辑只影响后续 Session。包级 Loader 与真实产品测试分别证明两个私有运行时、载荷缺失时不回退宿主命令、取消和进程树完全停稳。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema 与通用 Job 控制。
## 考虑过的替代方案
@@ -34,6 +34,6 @@ Claude Code Bundle 与显式 Codex Host 行都会在共享 Host 平面中恰好
## 后果
-用户只在需要 Claude Code 的 Profile 中安装该 Bundle;使用 Codex 的部署会显式挂载对应 Host 插件。模型可见授权仍通过与其他插件相同的 Agent Preset 创作路径管理。每个新 Session 会获得其 preset 工具行与 Host 可用提供方的交集。存在但未授权的产品保持休眠,会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录;缺席的产品不会进入提供方闭包。
+用户只安装 Profile 所需的产品 Bundle,并通过与其他插件相同的 Agent Preset 创作路径管理模型可见授权。每个新 Session 会获得其 preset 工具行与 Host 已安装提供方的交集。已安装但未授权的产品保持休眠,会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录;未安装的产品不会进入提供方或产品运行时闭包。
-Host 注册表仍是提供方的唯一权威,Profile Bundle 或显式 Host 组装仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
+Host 注册表仍是提供方的唯一权威,每个 Bundle 仍是部署可用性的权威,每个 Preset 仍是模型工具的权威。这个显式的双门生命周期避免全局启用开关,并让包移除与按会话创作保持独立。
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 77af3b4cb6..5ad029ab36 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.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: 29f438ca2ddcaea25bc7590fdca8879b80ada80c
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: dba880267c8d73cc34414910db029f17a0d7d6a4
+2026-08-04-claude-code-and-codex-subagent-backends.md: 9ea8ad65d8e5aaebf08753da3f4ef667cec2a236
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: a58f4c8273972499b90e29ff318bc7446491fd0e
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index 29f438ca2d..9ea8ad65d8 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.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) owns process-wide placement, the [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns both providers' default exclusion plus Claude Code's optional Bundle and Codex's explicit Host installation, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
+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, the [production-closure decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and default exclusion, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
@@ -34,7 +34,7 @@ fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product
## Codex provider
-`@deepseek-ai/dsh-subagent-codex` registers the fixed `codex` provider and starts `codex app-server --stdio` from `PATH`. Its public configuration contains only an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Installation, login, `CODEX_HOME`, model selection, base URL, sandbox, approval policy, and product-session settings remain native Codex or deployment responsibilities.
+`@deepseek-ai/dsh-subagent-codex` registers the fixed `codex` provider, resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains only an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Login, `CODEX_HOME`, model selection, base URL, sandbox, approval policy, and product-session settings remain native Codex or deployment responsibilities.
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize` → `initialized`, and creates an `ephemeral: true` thread. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
@@ -62,11 +62,11 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped explicit Profile configuration, verifies both fixed one-shot tools expose optional background scheduling alongside generic Job controls, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
-The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
+The Codex evidence pins `@openai/codex@0.147.0`, `codex-cli 0.147.0`, and all six optional platform aliases. Its real-product spec observes the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, wrapper/native whole-tree exit, and missing-payload failure without host fallback.
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
-The Claude Code evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader coverage resolves Codex through explicit Host composition and Claude Code through its optional Bundle while starting neither product.
+The Claude Code evidence pins Agent SDK 0.3.220, Claude Code 2.1.220, and the identities and versions of all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes the exact `x-api-key`, original task, byte-exact final answer, inherited temporary host-setting marker, process failure, local cancellation, and whole-tree exit. Unit coverage proves that production never resolves host `PATH`, omits the executable override, forwards the SDK-selected Windows `claude.exe` without a batch shim, and surfaces the SDK's missing-payload error without host fallback. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions. Loader coverage resolves both products through their optional Bundle patches while starting neither product.
The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented `deepseek-v4-pro[1m]` and `deepseek-v4-flash` model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.
@@ -90,6 +90,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
Users delegate through two stable one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
-Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Codex behavior depends on the deployment's host CLI and native configuration; Claude Code behavior depends on the Bundle-pinned platform CLI plus native account 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.
+Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Both products use Bundle-pinned platform CLIs plus native account and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index dba880267c..a58f4c8273 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.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)负责进程级放置,[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责两个提供方的默认发行排除、Claude Code 的可选 Bundle 与 Codex 的显式 Host 安装,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.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 及其默认发行排除,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs` 和 `dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
@@ -34,7 +34,7 @@ fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product
## Codex 提供方
-`@deepseek-ai/dsh-subagent-codex` 注册固定的 `codex` 提供方,并启动 `codex app-server --stdio`,该命令从 `PATH` 解析。其公开配置仅包含显式的 `env` 覆盖项和须为正有限值的 `disposeGraceMs`,且后者不得大于仓库共享的 `MAX_TIMER_DELAY_MS`。安装、登录、`CODEX_HOME`、模型选择、基础 URL、沙箱、审批策略和产品会话设置仍由 Codex 原生机制或部署环境负责。
+`@deepseek-ai/dsh-subagent-codex` 注册固定的 `codex` 提供方,解析锁定的 `@openai/codex@0.147.0` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置仅包含显式的 `env` 覆盖项和须为正有限值的 `disposeGraceMs`,且后者不得大于仓库共享的 `MAX_TIMER_DELAY_MS`。登录、`CODEX_HOME`、模型选择、基础 URL、沙箱、审批策略和产品会话设置仍由 Codex 原生机制或部署环境负责。
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize` → `initialized` 握手,并创建一个 `ephemeral: true` 线程。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
@@ -62,11 +62,11 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示的显式 Profile 配置,在同一个上下文中验证两个固定一次性工具会与通用 Job 控制工具一起公开可选后台调度,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
-Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
+Codex 证据会锁定 `@openai/codex@0.147.0`、`codex-cli 0.147.0` 与六个平台 alias。其真实产品测试会观测包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消、wrapper/原生整棵进程树退出,以及载荷缺失时不回退宿主命令的失败。
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
-Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 覆盖会通过显式 Host 组装解析 Codex,并通过可选 Bundle 解析 Claude Code,且不会启动任一产品。
+Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八个 SDK 平台包的身份与版本。真实产品测试会让 SDK 选择已安装载荷,断言共享子进程 argv 以该包的原生 CLI 开头,并观测确切的 `x-api-key`、原始任务、逐字节完全一致的最终回答、继承的临时宿主设置标记、进程失败、本地取消和整棵进程树退出。单元覆盖会证明生产运行从不解析宿主 `PATH`、省略可执行文件覆盖、直接转发 SDK 所选的 Windows `claude.exe` 而不经过 batch shim,并且在载荷缺失时原样暴露 SDK 错误且不回退宿主 CLI。这项证据证明锁定的官方 SDK/CLI 集成,而不证明与独立安装的 Claude 版本兼容。Loader 覆盖会通过各自的可选 Bundle patch 解析两个产品,且不会启动任一产品。
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
@@ -90,6 +90,6 @@ Claude Code 证据会锁定 Agent SDK 0.3.220、Claude Code 2.1.220,以及八
用户通过官方产品集成支持的两个稳定一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
-每次委派都要承担新建产品进程和独立模型上下文的开销。到达父级的产品载荷仍只有最终文本;后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。Codex 行为取决于部署环境的宿主 CLI 与原生配置;Claude Code 行为取决于 Bundle 锁定的平台 CLI,以及原生账户和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
+每次委派都要承担新建产品进程和独立模型上下文的开销。到达父级的产品载荷仍只有最终文本;后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。两个产品都使用 Bundle 锁定的平台 CLI,以及原生账户和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
index f842aecafe..6d277ed52d 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.i18n.yaml
+++ b/.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: 779bff6f668c086722f817a28a224182d0fbac09
-2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 27106668f8b81491d43e2033c1468799783c0584
+2026-08-12-production-dsh-excludes-product-subagent-providers.md: faa14a5848a421389d8c427f844c4dedc118a0fd
+2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 33be6118b7e5de261d22e62b1df98e1afad7a08c
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
index 779bff6f66..faa14a5848 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
@@ -6,17 +6,17 @@ English | [中文](2026-08-12-production-dsh-excludes-product-subagent-providers
## Problem
-`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code, including the Claude Agent SDK and its roughly 250 MB unpacked platform CLI payload, even when neither integration is used.
+`@deepseek-ai/dsh` receives the `@deepseek-ai/dsh-base` dependency closure. Including the Codex and Claude Code subagent providers there makes every production install download optional product integration code and large platform CLI payloads, even when neither integration is used.
## Decision
-This decision 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. The Claude Code provider package is 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 Codex package remains available for deployments that mount it explicitly.
+This decision partially supersedes only the default-inclusion part of the [shared-host placement](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md): `@deepseek-ai/dsh-base` does not depend on or mount the Codex and Claude Code subagent providers. Each provider package is a directly installable Profile Bundle whose `dsh.bundle.patch` points to one package-owned `cordis.patch.yml`. Each patch contributes exactly one self-provider Host row and no Agent tool row.
-The two optional integrations remain independent. Codex continues to use a host `codex` from `PATH`. The Claude Code Bundle owns the pinned Agent SDK and the matching platform CLI selected from the SDK's optional dependencies; production uses that private CLI and never falls back to a host `claude`. Installing the Claude Code Bundle does not pull in the Codex package, and the default `@deepseek-ai/dsh` production closure contains neither provider, the Claude Agent SDK, nor its platform payloads. The Bundle registers a dormant provider on the next Profile start, while an Agent Preset independently decides whether a new Session receives its tool. Installation brings only the Claude Code package closure onto disk; it does not start a product, authenticate an account, rewrite native settings, or grant model access.
+The two Bundles remain independent. The Codex Bundle owns the pinned official wrapper and six platform aliases; production starts the package-declared wrapper and never falls back to a host `codex`. The Claude Code Bundle owns the pinned Agent SDK and matching platform CLI; production lets the SDK select that private CLI and never falls back to a host `claude`. Installing one Bundle does not pull in the other, and the default `@deepseek-ai/dsh` production closure contains neither provider nor either product runtime. Each installed Bundle registers a dormant provider on the next Profile start, while an Agent Preset independently decides whether a new Session receives the corresponding tool. Installation does not start a product, authenticate an account, rewrite native settings, or grant model access.
## Verification
-Package tests pin the Claude Code Bundle manifest, published patch, exact self-provider row, and runtime closure. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform package identities and versions, the SDK-selected executable entering the shared subprocess owner, and first-delegation failure without host fallback when the payload is missing. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Production-closure tests prove the default and Claude-only dependency boundaries, while real Bundle-patch and Agent-Preset composition covers absent and installed Host states, disabled and enabled tool grants, later-Session adoption, and zero product processes. Existing Codex package tests continue to cover explicit Host composition and host executable resolution. The base bundle test continues to reject both provider dependencies and configuration rows.
+Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.220, Claude Code 2.1.220, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.147.0, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
## Alternatives considered
@@ -26,4 +26,4 @@ Package tests pin the Claude Code Bundle manifest, published patch, exact self-p
## Consequences
-Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. A Profile can add or remove the Claude Code provider Bundle directly; the changed Host availability takes effect on the next Profile start and explicitly accepts its SDK plus one large platform CLI payload. A Codex deployment still mounts that provider explicitly and supplies its product CLI through `PATH`. A separately authored Agent Preset grants either model-visible tool only to newly composed Sessions. No wrapper package, meta Bundle, dynamic installer, or persisted product-enable state is introduced.
+Installing `@deepseek-ai/dsh` does not download either product provider through the base bundle. A Profile can add or remove either provider Bundle independently; changed Host availability takes effect on the next Profile start, and selecting a product explicitly accepts its private platform payload. A separately authored Agent Preset grants either model-visible tool only to newly composed Sessions. No wrapper package beyond the products' official distributions, meta Bundle, dynamic installer, or persisted product-enable state is introduced.
diff --git a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
index 27106668f8..33be6118b7 100644
--- a/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
+++ b/.agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md
@@ -6,17 +6,17 @@ Status: implemented
## 问题
-`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码,包括 Claude Agent SDK 及其解包后约 250 MB 的平台 CLI 载荷,即使用户并未使用任一集成。
+`@deepseek-ai/dsh` 会获得 `@deepseek-ai/dsh-base` 的依赖闭包。如果 base 包含 Codex 与 Claude Code subagent 提供方,每次生产安装都会下载可选的产品集成代码与大型平台 CLI 载荷,即使用户并未使用任一集成。
## 决策
-本决策只部分取代[共享 host 放置决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)中关于默认包含提供方的部分:`@deepseek-ai/dsh-base` 不依赖也不挂载 Codex 与 Claude Code subagent 提供方。Claude Code 提供方包是可直接安装的 Profile Bundle,其 `dsh.bundle.patch` 指向包自身拥有的 `cordis.patch.yml`。该 patch 恰好贡献一条挂载自身提供方的 Host 行,不包含 Agent 工具行。Codex 包仍供部署环境显式挂载。
+本决策只部分取代[共享 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 工具行。
-两个可选集成彼此独立。Codex 继续使用 `PATH` 中的宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK,以及从 SDK optional dependencies 中选出的匹配平台 CLI;生产运行只使用该私有 CLI,绝不会回退到宿主 `claude`。安装 Claude Code Bundle 不会带入 Codex 包,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含 Claude Agent SDK 或其平台载荷。该 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装只会把 Claude Code 包闭包放到磁盘上;它不会启动产品、验证账户、改写原生设置或向模型授予访问权。
+两个 Bundle 彼此独立。Codex Bundle 自己负责锁定的官方 wrapper 与六个平台 alias;生产环境会启动包所声明的 wrapper,绝不会回退到宿主 `codex`。Claude Code Bundle 自己负责锁定的 Agent SDK 与匹配平台 CLI;生产环境让 SDK 选择该私有 CLI,绝不会回退到宿主 `claude`。安装其中一个 Bundle 不会带入另一个,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一提供方,也不包含任一产品运行时。每个已安装 Bundle 会在下次 Profile 启动时注册一个休眠提供方,而 Agent Preset 独立决定新 Session 是否获得对应工具。安装不会启动产品、验证账户、改写原生设置或向模型授予访问权。
## 验证
-包测试会固定 Claude Code Bundle 的 manifest、发布 patch、准确的自身提供方行以及运行时闭包。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包的身份与版本、SDK 所选可执行文件进入共享子进程责任方的路径,以及载荷缺失时第一次委派失败且不回退宿主 CLI。工作区验证会从 Bundle 声明派生每个发布 patch,而非维护包目录。生产闭包测试证明默认与仅 Claude 两种依赖边界;真实 Bundle patch 与 Agent Preset 组装会覆盖 Host 中缺席和已安装两种状态、禁用和启用两种工具授权、后续 Session 采纳以及零产品进程。现有 Codex 包测试继续覆盖显式 Host 组装和宿主可执行文件解析。base 组合包测试仍会拒绝这两个提供方依赖与配置行。
+包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.220、Claude Code 2.1.220、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.147.0、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
## 考虑过的替代方案
@@ -26,4 +26,4 @@ Status: implemented
## 后果
-安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以直接添加或移除 Claude Code provider Bundle;Host 可用性的变化会在下次 Profile 启动时生效,并代表明确接受其 SDK 与一个大型平台 CLI 载荷。Codex 部署仍须显式挂载该 provider,并通过 `PATH` 提供产品 CLI。单独创作的 Agent Preset 仍只会向新组装的 Session 授予任一模型可见工具。本决策不引入 wrapper 包、meta Bundle、动态安装程序或持久化的产品启用状态。
+安装 `@deepseek-ai/dsh` 时,不会通过 base 组合包下载任一产品提供方。Profile 可以独立添加或移除任一 provider Bundle;Host 可用性的变化会在下次 Profile 启动时生效,选择产品也代表明确接受其私有平台载荷。单独创作的 Agent Preset 仍只会向新组装的 Session 授予任一模型可见工具。本决策不会在产品官方发行版之外引入 wrapper 包,也不引入 meta Bundle、动态安装程序或持久化的产品启用状态。
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 92b218ff33..b03dad17cd 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -5,7 +5,7 @@
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
-This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
+This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code and Codex platform payload closures. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python closure is recorded separately in [`python/sdk/uv.lock`](python/sdk/uv.lock).
@@ -39,6 +39,7 @@ External packages that a workspace package resolves at runtime. The tier covers
| [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT |
| [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT |
| [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
+| [`@openai/codex`](https://github.com/openai/codex) | Apache-2.0 |
| [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
| [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
| [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 |
@@ -113,6 +114,20 @@ The installed SDK 0.3.220 declares the following optional platform packages. Eac
| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
+## Official Codex platform payloads
+
+The installed `@openai/codex` wrapper 0.147.0 declares the following optional-dependency aliases. Every alias resolves to an official platform-specific `@openai/codex` version that carries the native Codex CLI and its bundled resources; the declared license is verified against the payload installed for the current host.
+
+| Optional dependency alias | Published package | Version | Declared license |
+| --- | --- | --- | --- |
+| `@openai/codex-darwin-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-darwin-arm64) | 0.147.0-darwin-arm64 | Apache-2.0 |
+| `@openai/codex-darwin-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-darwin-x64) | 0.147.0-darwin-x64 | Apache-2.0 |
+| `@openai/codex-linux-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-linux-arm64) | 0.147.0-linux-arm64 | Apache-2.0 |
+| `@openai/codex-linux-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-linux-x64) | 0.147.0-linux-x64 | Apache-2.0 |
+| `@openai/codex-win32-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-win32-arm64) | 0.147.0-win32-arm64 | Apache-2.0 |
+| `@openai/codex-win32-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-win32-x64) | 0.147.0-win32-x64 | Apache-2.0 |
+
+
## Development-only npm dependencies
External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
@@ -122,7 +137,6 @@ External packages **directly declared** only by repository tooling, test infrast
| [`@braintree/sanitize-url`](https://github.com/braintree/sanitize-url) | MIT |
| [`@modelcontextprotocol/server-everything`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
| [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers) | MIT / Apache-2.0 |
-| [`@openai/codex`](https://github.com/openai/codex) | Apache-2.0 |
| [`@stylistic/eslint-plugin`](https://github.com/eslint-stylistic/eslint-stylistic) | MIT |
| [`@testing-library/dom`](https://github.com/testing-library/dom-testing-library) | MIT |
| [`@testing-library/react`](https://github.com/testing-library/react-testing-library) | MIT |
diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml
index 581f51773e..eedabe4cd7 100644
--- a/apps/cli/config/agent-presets/code/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/code/agent.cordis.yml
@@ -198,10 +198,10 @@
toolName: subagent_fork
backgroundMode: continuable
- # Production dsh does not install these optional providers. A deployment
- # mounts Codex explicitly; the Claude Code Bundle mounts its provider once
- # on the host plane. Copy this preset, then remove `disabled` from the
- # matching tool row; Host availability alone grants no tool.
+ # Production dsh does not install these optional providers. Install the
+ # matching Bundle in this Profile and restart the Host, then copy this
+ # preset and remove `disabled` from the matching tool row. Host availability
+ # alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
index db5f01021f..aef4a250e4 100644
--- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml
@@ -185,10 +185,10 @@
toolName: subagent_fork
backgroundMode: continuable
- # Production dsh does not install these optional providers. A deployment
- # mounts Codex explicitly; the Claude Code Bundle mounts its provider once
- # on the host plane. Copy this preset, then remove `disabled` from the
- # matching tool row; Host availability alone grants no tool.
+ # Production dsh does not install these optional providers. Install the
+ # matching Bundle in this Profile and restart the Host, then copy this
+ # preset and remove `disabled` from the matching tool row. Host availability
+ # alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
index 3a1fb24947..ed99349d95 100644
--- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
+++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md
@@ -123,16 +123,16 @@ After a clean mount-validation, ask the user to start a session on the new prese
## Native product subagents
-The Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider:
+Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers:
```sh
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-The Bundle owns Claude Code 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 the package withdraws the provider on the next Profile start.
-
-Codex remains an explicitly mounted Host plugin rather than a directly installable Bundle. A deployment that uses it must install and mount the package once on the Host plane before a preset can expose its tool.
+Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start.
Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:
@@ -156,7 +156,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 available product tool. The Claude Code row requires its Bundle, while the Codex row requires an explicit Host composition and a host `codex` on `PATH`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.
+The two rows are independent. Leaving both disabled preserves the copied preset; enabling one exposes only that installed product tool. The Codex Bundle exclusively uses the wrapper and native platform payload selected by its pinned official package, while the Claude Code Bundle exclusively uses the platform CLI selected by its pinned Agent SDK. Neither provider inspects or falls back to a host product command, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing a product Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.
## What not to move into a preset
diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml
index 4637293576..3bccbc7365 100644
--- a/apps/cli/config/agent-presets/standard/agent.cordis.yml
+++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml
@@ -197,10 +197,10 @@
toolName: subagent_fork
backgroundMode: continuable
- # Production dsh does not install these optional providers. A deployment
- # mounts Codex explicitly; the Claude Code Bundle mounts its provider once
- # on the host plane. Copy this preset, then remove `disabled` from the
- # matching tool row; Host availability alone grants no tool.
+ # Production dsh does not install these optional providers. Install the
+ # matching Bundle in this Profile and restart the Host, then copy this
+ # preset and remove `disabled` from the matching tool row. Host availability
+ # alone grants no tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml
index 0a5792a21c..ef63dd2fc6 100644
--- a/apps/cli/reference/README.i18n.yaml
+++ b/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: c6c3f61c2910b84b9364ef7eaa13b8c0c011999b
-README.zh.md: d6f3e7f1f508724e54e59df01e93199193ddc506
+README.md: 7828f55a2e4adfd85a0018baada6945ea75aacb0
+README.zh.md: e14e13731c314efd4d39913b91f2e90ba624e55c
diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md
index c6c3f61c29..7828f55a2e 100644
--- a/apps/cli/reference/README.md
+++ b/apps/cli/reference/README.md
@@ -42,14 +42,17 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` 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 Claude Code subagent provider is an optional Bundle. Add or remove it independently:
+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 add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile 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 the Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, the installed package registers only its dormant Host provider and starts no Claude process. The Bundle installs the pinned Agent SDK and one matching private platform CLI, uses only that CLI, and never falls back to a host `claude`. Authentication and native Claude settings remain user-managed; the [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) discloses the current platform payload size and missing-payload failure. Full Agent Presets keep both product tool rows disabled, so a copied Preset must separately enable the Claude Code row before a new Agent can see that tool. The Codex provider remains an explicitly mounted Host plugin that resolves `codex` from `PATH`; the default dsh dependency closure includes neither provider nor the Claude Agent SDK or its platform payloads.
+The successful pnpm operation changes the Profile manifest and Bundle list on disk; a running Profile keeps the Bundle set from its current start. Restart that Profile after adding, removing, or updating a Bundle. This startup boundary applies to Bundle membership, while ordinary edits to the Profile or home `cordis.patch.yml` take effect through hot reload. On the next start, each installed Bundle registers only its dormant Host provider; a copied Preset must separately enable the matching tool row for new Agents. The [Codex provider README](../../../packages/subagent/subagent-codex/README.md) and [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md) own executable, authentication, payload, and failure details; the [base Bundle reference](../../../packages/bundle/base/README.md) owns the default dependency closure.
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md
index d6f3e7f1f5..e14e13731c 100644
--- a/apps/cli/reference/README.zh.md
+++ b/apps/cli/reference/README.zh.md
@@ -42,14 +42,17 @@ dsh --profile web --patch ./extra.yml --dump-config
`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `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` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。
-Claude Code subagent provider 是一个可选 Bundle,可以独立添加或移除:
+Codex 与 Claude Code subagent provider 是两个彼此独立的可选 Bundle。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
```sh
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code
```
-pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,已安装的包只注册休眠的 Host provider,不会启动 Claude 进程。该 Bundle 会安装锁定的 Agent SDK 与一个匹配的私有平台 CLI,只使用该 CLI,并且绝不会回退到宿主 `claude`。Claude 的身份验证与原生设置仍由用户管理;[Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)会披露当前平台载荷体积与载荷缺失时的失败行为。完整 Agent Preset 中的两个产品工具行仍默认禁用,因此还须在复制出的 Preset 中单独启用 Claude Code 行,新 Agent 才能看到该工具。Codex provider 仍须作为 Host 插件显式挂载,并从 `PATH` 解析 `codex`;默认 dsh 依赖闭包不包含任一 provider,也不包含 Claude Agent SDK 或其平台载荷。
+pnpm 操作成功后只会改变磁盘上的 Profile manifest 与 Bundle 列表;正在运行的 Profile 会保留本次启动时的 Bundle 集合。添加、移除或更新 Bundle 后须重启该 Profile。这个启动边界只适用于 Bundle 成员变化,Profile 或 home 中普通 `cordis.patch.yml` 的编辑通过热重载生效。下一次启动时,每个已安装 Bundle 只注册自己的休眠 Host provider;还须在复制出的 Preset 中单独启用对应工具行,新 Agent 才能看到该工具。[Codex provider README](../../../packages/subagent/subagent-codex/README.md)与 [Claude Code provider README](../../../packages/subagent/subagent-claude-code/README.md)负责可执行文件、身份验证、载荷与失败细节;[base Bundle 参考](../../../packages/bundle/base/README.md)负责默认依赖闭包。
```sh
dsh plugin --profile tui add github:deepseek-harness/turtle-ui
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index b4e875c07d..5e9680fd13 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -26,6 +26,7 @@ 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')
@@ -443,11 +444,12 @@ describe('the shipped Web composition', () => {
})
})
-describe('Claude Code Bundle and user-preset intersection', () => {
- const presetIds = ['products-none', 'products-claude'] as const
+describe('product Bundle and user-preset intersection', () => {
+ const presetIds = ['products-none', 'products-codex', 'products-claude', 'products-both'] as const
+ type Product = 'codex' | 'claude-code'
type PresetId = typeof presetIds[number]
- async function bootProducts(installed: boolean): Promise {
+ async function bootProducts(installed: readonly Product[]): Promise {
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
const userRoot = join(root, 'presets')
const settingsFile = join(root, 'settings.yaml')
@@ -455,14 +457,22 @@ describe('Claude Code Bundle and user-preset intersection', () => {
await writeFile(settingsFile, '{}\n')
for (const id of presetIds) {
let composition = standard
- if (id === 'products-claude') {
+ if (id === 'products-codex' || id === 'products-both') {
+ composition = enablePresetTool(composition, 'tool-subagent-codex')
+ }
+ if (id === 'products-claude' || id === 'products-both') {
composition = enablePresetTool(composition, 'tool-subagent-claude-code')
}
const directory = join(userRoot, id)
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'agent.cordis.yml'), composition)
}
- const productPatches = installed ? loadOverlayPatches('dsh-test', CLAUDE_CODE_PATCH) : []
+ const patchPath = (product: Product): string => (
+ product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH
+ )
+ const productPatches = installed.flatMap(product => (
+ loadOverlayPatches('dsh-test', patchPath(product))
+ ))
return await bootWeb(settingsFile, [
...productPatches,
{
@@ -476,13 +486,21 @@ describe('Claude Code Bundle and user-preset intersection', () => {
includeUserRoot: false,
},
},
- ], installed ? [dirname(CLAUDE_CODE_PATCH)] : [])
+ ], installed.map(product => dirname(patchPath(product))))
}
- it('composes the intersection of the installed Bundle and enabled preset row', async () => {
- const scenarios: Array<{ installed: boolean; presets: readonly PresetId[] }> = [
- { installed: false, presets: presetIds },
- { installed: true, presets: presetIds },
+ it('composes the intersection of installed Bundles and enabled preset rows', async () => {
+ const enabledByPreset: Record = {
+ 'products-none': [],
+ 'products-codex': ['codex'],
+ 'products-claude': ['claude-code'],
+ 'products-both': ['codex', 'claude-code'],
+ }
+ const scenarios: Array<{ installed: Product[]; presets: readonly PresetId[] }> = [
+ { installed: [], presets: ['products-both'] },
+ { installed: ['codex'], presets: ['products-both'] },
+ { installed: ['claude-code'], presets: ['products-both'] },
+ { installed: ['codex', 'claude-code'], presets: presetIds },
]
for (const { installed, presets } of scenarios) {
@@ -492,16 +510,17 @@ describe('Claude Code Bundle and user-preset intersection', () => {
expect(productCtx.subagents.list()
.filter(name => name === 'codex' || name === 'claude-code')
.sort())
- .toEqual(installed ? ['claude-code'] : [])
+ .toEqual([...installed].sort())
for (const id of presets) {
const handle = await productCtx.agents.create({
- sessionId: SessionId(`preset-${id}-${installed ? 'claude' : 'none'}-${randomUUID()}`),
+ sessionId: SessionId(`preset-${id}-${installed.join('-') || 'none'}-${randomUUID()}`),
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, id).then(() => undefined),
})
try {
- const productTools = installed && id === 'products-claude'
- ? ['subagent_claude_code']
- : []
+ const productTools = enabledByPreset[id]
+ .filter(product => installed.includes(product))
+ .map(product => product === 'codex' ? 'subagent_codex' : 'subagent_claude_code')
+ .sort()
const tools = toolNames(productCtx, handle.agent)
expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code'))
.toEqual(productTools)
@@ -524,7 +543,7 @@ describe('Claude Code Bundle and user-preset intersection', () => {
}, 120_000)
it('applies a product-row edit only to later sessions on the preset', async () => {
- const productCtx = await bootProducts(true)
+ 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({
@@ -532,16 +551,16 @@ describe('Claude Code Bundle and user-preset intersection', () => {
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
})
try {
- expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_claude_code')
- await writeFile(preset.path, enablePresetTool(original, 'tool-subagent-claude-code'))
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
+ await writeFile(preset.path, enablePresetTool(original, 'tool-subagent-codex'))
const later = await productCtx.agents.create({
sessionId: SessionId('preset-product-generation-later'),
setup: agentCtx => productCtx.agentPresets.mount(agentCtx, 'products-none').then(() => undefined),
})
try {
- expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_claude_code')
- expect(toolNames(productCtx, later.agent)).toContain('subagent_claude_code')
+ expect(toolNames(productCtx, existing.agent)).not.toContain('subagent_codex')
+ expect(toolNames(productCtx, later.agent)).toContain('subagent_codex')
} finally {
await later.dispose()
}
diff --git a/apps/web/tests/skill-tool-row.e2e.ts b/apps/web/tests/skill-tool-row.e2e.ts
index 3e18ff9548..ca19ce4204 100644
--- a/apps/web/tests/skill-tool-row.e2e.ts
+++ b/apps/web/tests/skill-tool-row.e2e.ts
@@ -63,7 +63,7 @@ describe.skipIf(MODE === 'record')('web e2e: dedicated Skill tool row', () => {
const output = call.locator('pre')
await output.waitFor()
expect(await output.textContent()).toContain('')
- expect(await output.textContent()).toContain('The Claude Code Bundle installs and exclusively uses the matching platform CLI')
+ expect(await output.textContent()).toContain('Codex and Claude Code providers are independent optional Profile Bundles')
expect(await output.evaluate(element => getComputedStyle(element.parentElement!).maxHeight)).toBe('260px')
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
diff --git a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
index f4acc2a978..6f8dc183d7 100644
--- a/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
+++ b/apps/web/tests/snapshots/skill-tool-row/ui.expected.md
@@ -25,7 +25,7 @@
- button "Skill editing-cordis-compositions" [expanded]:
- img
- text: Skill editing-cordis-compositions
-- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created. ## The roster service `ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. Read `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on: - `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. - `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). ```js return { name: 'preset-tools', inject: ['agentPresets', 'tools'], apply(ctx) { harness.registerTool(ctx, harness.defineTool({ name: 'preset_check', description: 'Mount-validate one preset by id.', parameters: { id: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, async execute(args) { try { await ctx.agentPresets.standingKeyFor(args.id) return 'mounted OK' } catch (error) { return error.message } }, })) }, } ``` Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset 1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. ## The rule that catches people **A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: ```yaml - id: delegation name: cordis:group group: true isolate: workflows: true config: - id: workflow-worker-thread name: '@deepseek-ai/dsh-workflow-worker-thread' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` `true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. ## Verifying a change **`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); - a row that never activated (`N row(s) did not activate: : waiting for `); - a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. **Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. `cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. `cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. ## Native product subagents The Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider: ```sh dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code ``` The Bundle owns Claude Code 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 the package withdraws the provider on the next Profile start. Codex remains an explicitly mounted Host plugin rather than a directly installable Bundle. A deployment that uses it must install and mount the package once on the Host plane before a preset can expose its tool. Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code backgroundMode: one-shot maxDepth: provider-managed ``` The two rows are independent. Leaving both disabled preserves the copied preset; enabling one exposes only that available product tool. The Claude Code row requires its Bundle, while the Codex row requires an explicit Host composition and a host `codex` on `PATH`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. "
+- region "Instructions": "Instructions Base directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. # Editing Cordis compositions Every capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it. ## Off-limits **Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation. To change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete. ## Decide the plane first Two planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared. **Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process. **Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it. **A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side. A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name. Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created. ## The roster service `ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step. Read `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on: - `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent. - `read(id)` — one preset's composition text, without a file tool or a path. - `copy(from, id, name?)` — the only authoring write (see below). - `standingKeyFor(id)` — mount-validate one preset (see below). ```js return { name: 'preset-tools', inject: ['agentPresets', 'tools'], apply(ctx) { harness.registerTool(ctx, harness.defineTool({ name: 'preset_check', description: 'Mount-validate one preset by id.', parameters: { id: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } }, async execute(args) { try { await ctx.agentPresets.standingKeyFor(args.id) return 'mounted OK' } catch (error) { return error.message } }, })) }, } ``` Unmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind. ## Authoring a preset 1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source. 2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do. 3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`. 4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule. 5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*. A composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable. ## The rule that catches people **A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later. Whether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service. When a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here: ```yaml - id: delegation name: cordis:group group: true isolate: workflows: true config: - id: workflow-worker-thread name: '@deepseek-ai/dsh-workflow-worker-thread' config: provider: spawn - id: tool-workflow name: '@deepseek-ai/dsh-tool-workflow' ``` `true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs. A consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated. Realms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm. ## Verifying a change **`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails: - a row whose package does not resolve (`Cannot find package …`); - a row whose config is invalid (`invalid config: $. missing required value`); - a row that never activated (`N row(s) did not activate: : waiting for `); - a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service. It returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind. **Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition. `cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do. After a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces. `cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file. ## Native product subagents Codex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers: ```sh dsh plugin --profile add @deepseek-ai/dsh-subagent-codex dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex dsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code ``` Each Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start. Copy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested: ```yaml - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code backgroundMode: one-shot maxDepth: provider-managed ``` The two rows are independent. Leaving both disabled preserves the copied preset; enabling one exposes only that installed product tool. The Codex Bundle exclusively uses the wrapper and native platform payload selected by its pinned official package, while the Claude Code Bundle exclusively uses the platform CLI selected by its pinned Agent SDK. Neither provider inspects or falls back to a host product command, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing a product Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings. ## What not to move into a preset `agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement. "
- button "Inspect"
- button "Think The skill is loaded.":
- img
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index 353df8d744..2e9e666180 100644
--- a/docs/module-graph.i18n.yaml
+++ b/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: 24ee9a4815b4236336ae37f6c926dba4718dafb9
-module-graph.zh.md: d87feb946a43a2f9d391e7894b4a143af4e18406
+module-graph.md: 46f924b0df80aa667d4929e6d2707c8cf051bfb2
+module-graph.zh.md: 3a226bbdacfd1a5b850a406bea66bf29a5db209b
diff --git a/docs/module-graph.md b/docs/module-graph.md
index 24ee9a4815..46f924b0df 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -961,6 +961,12 @@ flowchart TD
pkg_subagent_claude_code --> pkg_subagent
pkg_subagent_claude_code --> pkg_subprocess
pkg_subagent_claude_code --> pkg_timeout
+ pkg_subagent_codex --> pkg_invariants
+ pkg_subagent_codex --> pkg_llm
+ pkg_subagent_codex --> pkg_session
+ pkg_subagent_codex --> pkg_subagent
+ pkg_subagent_codex --> pkg_subprocess
+ pkg_subagent_codex --> pkg_timeout
pkg_subagent_in_process_driver --> pkg_agent
pkg_subagent_in_process_driver --> pkg_invariants
pkg_subagent_in_process_driver --> pkg_llm
@@ -1068,13 +1074,6 @@ flowchart TD
pkg_workflow_worker_thread --> pkg_subagent
pkg_workflow_worker_thread --> pkg_tools
pkg_workflow_worker_thread --> pkg_workflow
- pkg_subagent_codex --> pkg_invariants
- pkg_subagent_codex --> pkg_llm
- pkg_subagent_codex --> pkg_sdk_protocol
- pkg_subagent_codex --> pkg_session
- pkg_subagent_codex --> pkg_subagent
- pkg_subagent_codex --> pkg_subprocess
- pkg_subagent_codex --> pkg_timeout
pkg_subagent_fork_in_process --> pkg_agent
pkg_subagent_fork_in_process --> pkg_invariants
pkg_subagent_fork_in_process --> pkg_session
@@ -1577,6 +1576,7 @@ flowchart TD
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1592,7 +1592,6 @@ flowchart TD
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index d87feb946a..3a226bbdac 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -963,6 +963,12 @@ flowchart TD
pkg_subagent_claude_code --> pkg_subagent
pkg_subagent_claude_code --> pkg_subprocess
pkg_subagent_claude_code --> pkg_timeout
+ pkg_subagent_codex --> pkg_invariants
+ pkg_subagent_codex --> pkg_llm
+ pkg_subagent_codex --> pkg_session
+ pkg_subagent_codex --> pkg_subagent
+ pkg_subagent_codex --> pkg_subprocess
+ pkg_subagent_codex --> pkg_timeout
pkg_subagent_in_process_driver --> pkg_agent
pkg_subagent_in_process_driver --> pkg_invariants
pkg_subagent_in_process_driver --> pkg_llm
@@ -1070,13 +1076,6 @@ flowchart TD
pkg_workflow_worker_thread --> pkg_subagent
pkg_workflow_worker_thread --> pkg_tools
pkg_workflow_worker_thread --> pkg_workflow
- pkg_subagent_codex --> pkg_invariants
- pkg_subagent_codex --> pkg_llm
- pkg_subagent_codex --> pkg_sdk_protocol
- pkg_subagent_codex --> pkg_session
- pkg_subagent_codex --> pkg_subagent
- pkg_subagent_codex --> pkg_subprocess
- pkg_subagent_codex --> pkg_timeout
pkg_subagent_fork_in_process --> pkg_agent
pkg_subagent_fork_in_process --> pkg_invariants
pkg_subagent_fork_in_process --> pkg_session
@@ -1579,6 +1578,7 @@ flowchart TD
| [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
@@ -1594,7 +1594,6 @@ flowchart TD
| [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`workflow-worker-thread`](../packages/workflow/workflow-worker-thread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
-| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
index 6afe2b888d..c70374f83d 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml
@@ -1,5 +1,5 @@
-# Test-only composition of the public opt-in provider and one-shot task tool.
-# The owning e2e boots this tree but never invokes the model or Codex.
+# Test-only composition of the Codex one-shot tool around its 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:
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
index 51cd5eaa6f..25040faea5 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts
+++ b/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
diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
index 89aea22173..5d03979af6 100644
--- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl
@@ -19,7 +19,7 @@
{"type":"assistant/chunk","seq":17,"time":1785730426828,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":18,"time":1785730426828,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3fd7a47e-84c9-4d31-aa95-9939671ba0a5"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[10,11,12,13,14,15,16,17],"surfaceOp":"append"}
{"type":"tool/call","seq":19,"time":1785730426828,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"editing-cordis-compositions\"}"}}
-{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nThe Claude Code provider is an optional Profile Bundle. Install it only in Profiles that need it, then restart the Profile so its Host registers the provider:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\ndsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code\n```\n\nThe Bundle owns Claude Code 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 the package withdraws the provider on the next Profile start.\n\nCodex remains an explicitly mounted Host plugin rather than a directly installable Bundle. A deployment that uses it must install and mount the package once on the Host plane before a preset can expose its tool.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n backgroundMode: one-shot\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n backgroundMode: one-shot\n maxDepth: provider-managed\n```\n\nThe two rows are independent. Leaving both disabled preserves the copied preset; enabling one exposes only that available product tool. The Claude Code row requires its Bundle, while the Codex row requires an explicit Host composition and a host `codex` on `PATH`. The Claude Code Bundle installs and exclusively uses the matching platform CLI selected by its pinned Agent SDK; it does not inspect or fall back to a host `claude`, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing the Claude Code Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"e710fcbb-f128-463c-8db2-f90cdc0ad9b8"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
+{"type":"tool/result","seq":20,"time":1785730426838,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_skill_load"},"content":[{"type":"tool-result","toolCallId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: {{cwd}}/.dsh/skills/editing-cordis-compositions\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\n# Editing Cordis compositions\n\nEvery capability in this harness is a plugin row in a `cordis.yml`. There is no separate configuration language: changing what an agent can do means changing which rows are composed for it.\n\n## Off-limits\n\n**Never edit, delete, or overwrite a preset that ships with the deployment** — the `agent-presets` directory beside the deployment's own config, which supplies `standard`, `code`, `minimal`, and `cordis`. Never escalate the sandbox to reach it, even when a change there looks quicker. An upgrade overwrites that install, and corrupting `cordis` disables preset authoring itself. Reading a shipped composition is the intended way to start; writing to one is not, and neither is editing the host composition to work around a preset limitation.\n\nTo change what a shipped preset does, copy it and edit the copy. Locally authored presets under the user root are yours to create, edit, and delete.\n\n## Decide the plane first\n\nTwo planes, and the choice is not about how \"agent-related\" something feels — it is about whether the thing must be shared.\n\n**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.\n\n**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.\n\n**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.\n\nA preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.\n\nLocally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.\n\n## The roster service\n\n`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.\n\nRead `cordis_inspect what:\"api\" name:\"agentPresets\"` for the current signatures before writing the code. What this skill relies on:\n\n- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.\n- `read(id)` — one preset's composition text, without a file tool or a path.\n- `copy(from, id, name?)` — the only authoring write (see below).\n- `standingKeyFor(id)` — mount-validate one preset (see below).\n\n```js\nreturn {\n name: 'preset-tools',\n inject: ['agentPresets', 'tools'],\n apply(ctx) {\n harness.registerTool(ctx, harness.defineTool({\n name: 'preset_check',\n description: 'Mount-validate one preset by id.',\n parameters: { id: { type: 'string', required: true } },\n output: { schema: { type: 'string' }, render(_a, v) { return [{ type: 'text', text: v }] } },\n async execute(args) {\n try {\n await ctx.agentPresets.standingKeyFor(args.id)\n return 'mounted OK'\n } catch (error) {\n return error.message\n }\n },\n }))\n },\n}\n```\n\nUnmount the plugin with `cordis_unmount` when you are done; it is a probe, not a capability to leave behind.\n\n## Authoring a preset\n\n1. **Start from a copy.** `copy(from, id, name)` copies a whole preset directory into the user root — composition, metadata, skill directories, assets. It validates the id against `[a-z0-9][a-z0-9-]*` (it becomes the directory name, so no leading hyphen), refuses an id any root already supplies, rolls a failed copy back, and rewrites the copy's `preset.yml` to keep the source's description while dropping its name and roster `order`. Prefer it over a shell copy: it needs no sandbox escalation, it lands the copy in whichever root this deployment made writable, and the copy is exactly as loadable as its source. `resolve(id)` then names the file it created — that path, not a guessed one, is what the following edits target. `standard` is the full coding agent and the usual source.\n2. **Expect the file sandbox on every edit after the copy.** The user preset root lies outside the session workspace, so under the default `workspace-write` policy the first write there is denied. Only writes are: reading any composition by absolute path needs no escalation. Retry that exact command once with `sandbox_permissions` escalation and a short justification — the user sees and approves it. Batch your writes (one heredoc per file) rather than escalating many small commands. `copy()` itself runs host-side and needs none of this; the edits do.\n3. **Write the copy's `description`** in `preset.yml`, and its `name` if you passed none to `copy()`.\n4. **Edit `agent.cordis.yml`** row by row, keeping the plane rule and the realm rule.\n5. **Mount-validate the result**, then hand off to the user for a real session — both under *Verifying a change*.\n\nA composition written from scratch usually forgets a group realm or a consumer row; a copy starts loadable.\n\n## The rule that catches people\n\n**A row that publishes a service may not sit loose in a preset.** Registering a service without an isolate realm puts it in the process-global realm, so the second session mounting that preset collides with the first. The mount rejects it rather than letting the collision surface later.\n\nWhether a row publishes a service is not visible from its name, and package READMEs are absent from an installed deployment. Read it off the live runtime instead: `cordis_inspect what:\"services\"` lists every service with the fiber that owns it, so a service attributed to a fiber other than the row you are adding is one that row consumes rather than provides. For a row not in your current composition, mount-validate and read the rejection — it names the offending service.\n\nWhen a preset genuinely owns a service, wrap the provider **and every consumer that reaches it** in one group carrying an `isolate` realm. The shipped `standard` composition does this for `workflows`, which nothing outside an agent reads — its `delegation` group, with the delegation tools omitted here:\n\n```yaml\n- id: delegation\n name: cordis:group\n group: true\n isolate:\n workflows: true\n config:\n - id: workflow-worker-thread\n name: '@deepseek-ai/dsh-workflow-worker-thread'\n config:\n provider: spawn\n - id: tool-workflow\n name: '@deepseek-ai/dsh-tool-workflow'\n```\n\n`true` means a realm private to each mounting session. A string label instead joins subtrees into one shared realm; `provide()` still throws on the second registration under that symbol, so a label does not pool instances and is not what a preset needs.\n\nA consumer left outside the group resolves the host's registry, which the preset did not populate, and then contributes nothing. Mount-validation catches that as a row that never activated.\n\nRealms are for services a preset owns, not for every group. A host capability the preset only consumes must stay outside a realm, or the row cannot resolve it: `tool-bash`, `tool-jobs`, and `tool-goal` publish nothing and sit loose in `standard`, which explains in comments which host instance each one resolves and why a realm would break it. Wrapping a consumer row in a realm of its own is the same error as leaving one outside its provider's realm.\n\n## Verifying a change\n\n**`standingKeyFor(id)` is the check.** It composes the preset's plugin subtree for real — the same mount a session start performs, minus the agent — and rejects the four ways a composition fails:\n\n- a row whose package does not resolve (`Cannot find package …`);\n- a row whose config is invalid (`invalid config: $. missing required value`);\n- a row that never activated (`N row(s) did not activate: : waiting for `);\n- a service published into the root realm, which arrives as one of two messages. A name the host does not supply lands in the root realm and the mount audit rejects it: `row(s) published process-global service(s) []; a preset service must sit behind an isolate realm or move to the host composition` — this is the shape a preset's own forgotten realm takes. A name the host already supplies collides before the audit: `service \"\" has been registered at `. Both name the offending service.\n\nIt returns normally when the composition mounts. Run it as the final check on a finished edit rather than after every line: a successful mount installs a standing generation that lives until the process exits, while a failed one disposes its subtree and leaves nothing behind.\n\n**Do not treat the roster's `broken` field as validation.** `list()` reports `broken` from a shape check — the file parses in the loader's YAML dialect and holds named rows — which every failure above passes. It catches a damaged file, not an unusable composition.\n\n`cordis_inspect` reports THIS session's composition, so it confirms what a row does in the runtime you are already in, never what your new preset will do.\n\nAfter a clean mount-validation, ask the user to start a session on the new preset and confirm the tool list; the preset decides tool schemas and prompt sections, and only a real session shows the agent that composition produces.\n\n`cordis_mount` evaluates JavaScript against the live runtime and disappears on restart. It is for probing, not for shipping a capability: a capability belongs in a composition file.\n\n## Native product subagents\n\nCodex and Claude Code providers are independent optional Profile Bundles. Install only the products a Profile needs, then restart the Profile so its Host registers those providers:\n\n```sh\ndsh plugin --profile add @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code\ndsh plugin --profile remove @deepseek-ai/dsh-subagent-codex\ndsh plugin --profile remove @deepseek-ai/dsh-subagent-claude-code\n```\n\nEach Bundle owns its Host availability; the preset separately grants one Agent its ordinary delegation tool. Never move a product provider into the preset and never add a product-specific settings field. Removing one package withdraws only that provider on the next Profile start.\n\nCopy these disabled templates from a shipped full preset and remove `disabled` only for the products the user requested:\n\n```yaml\n- id: tool-subagent-codex\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: codex\n toolName: subagent_codex\n backgroundMode: one-shot\n maxDepth: provider-managed\n\n- id: tool-subagent-claude-code\n name: '@deepseek-ai/dsh-tool-subagent'\n disabled: true\n config:\n provider: claude-code\n toolName: subagent_claude_code\n backgroundMode: one-shot\n maxDepth: provider-managed\n```\n\nThe two rows are independent. Leaving both disabled preserves the copied preset; enabling one exposes only that installed product tool. The Codex Bundle exclusively uses the wrapper and native platform payload selected by its pinned official package, while the Claude Code Bundle exclusively uses the platform CLI selected by its pinned Agent SDK. Neither provider inspects or falls back to a host product command, and a missing optional payload fails the first delegation. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Job id. Full presets already carry `tool-jobs`, while the base Host carries the job registry; retain both so `job_output`, `job_list`, `job_kill`, cancellation, and completion notices stay available. Neither installing a product Bundle nor composing either preset row starts a product, authenticates an account, selects a model, probes credentials, or manages native product settings.\n\n## What not to move into a preset\n\n`agent-loop` registers the one agent factory and throws on a second. The registries own the per-session layering and cannot themselves be per-session. Session persistence must stay host-side or the session list fragments. The sandbox, approval, and permission rows are a deliberate boundary: a preset is exactly as privileged as the plugins it names, so letting one relax its own confinement would defeat the confinement.\n\n"}],"isError":false}],"role":"user","id":"cfdab17c-645d-4114-956f-1d91776289f5"}},"sourceEventSeqs":[19],"surfaceOp":"append"}
{"type":"step/end","seq":21,"time":1785730426838,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":22,"time":1785730426848,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
diff --git a/packages/bundle/README.i18n.yaml b/packages/bundle/README.i18n.yaml
index b42a5e5d52..0441ec7d87 100644
--- a/packages/bundle/README.i18n.yaml
+++ b/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: 3afa53c1444b43c38b7a71f3e9e00d271078be54
-README.zh.md: 740b3579ce1555f2b1b26ca79e8a3d915a338193
+README.md: 4d7a064939ae04f25737b324ec35332b7b944f80
+README.zh.md: 8910b33a97acd2ef3ee5b659305739246004de01
diff --git a/packages/bundle/README.md b/packages/bundle/README.md
index 3afa53c144..4d7a064939 100644
--- a/packages/bundle/README.md
+++ b/packages/bundle/README.md
@@ -4,7 +4,7 @@ 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 [Claude Code subagent package](../subagent/subagent-claude-code/README.md) is a directly installable example.
+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 |
|---|---|---|
diff --git a/packages/bundle/README.zh.md b/packages/bundle/README.zh.md
index 740b3579ce..8910b33a97 100644
--- a/packages/bundle/README.zh.md
+++ b/packages/bundle/README.zh.md
@@ -4,7 +4,7 @@
Profile 组合包:在 manifest(元数据清单)中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包,因此可作为 patch 层安装进 `dsh --profile` 组合([profile 约定](../boot/app-boot/README.md#profiles))。组合包的实体是它的 patch 列表;有些组合包还附带由其 patch 挂载的运行时粘合插件。
-Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包可以携带自己的可选 Profile 层;[Claude Code subagent 包](../subagent/subagent-claude-code/README.md)就是可直接安装的例子。
+Bundle 身份由 manifest 声明决定,而不是由本目录决定。领域包可以携带自己的可选 Profile 层;[Codex 与 Claude Code subagent 包](../subagent/README.md)就是可直接安装的例子。
| 包 | 职责 | ctx key |
|---|---|---|
diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml
index ea1fb9b03a..05ac3dfe47 100644
--- a/packages/bundle/base/README.i18n.yaml
+++ b/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: 5fdd642ecc03fea77b2b00fc6428525c1a40d891
-README.zh.md: c2a07ec15816e41eadf682bcd8631c93bce77ae0
+README.md: 8487426ee7bf1b39a79b4e80b9c7bd661f317998
+README.zh.md: 797968d362cd35d1ba04087b30d8d02d1b537b44
diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md
index 5fdd642ecc..8487426ee7 100644
--- a/packages/bundle/base/README.md
+++ b/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 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 can install the [Claude Code provider Bundle](../../subagent/subagent-claude-code/README.md) only when needed, while a deployment that uses Codex still mounts that provider explicitly. 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 shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, telemetry, and the core spawn/fork subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. The optional Codex and Claude Code providers stay outside this package and its production dependency closure; a Profile installs either [product provider Bundle](../../subagent/README.md) only when needed. The default `@deepseek-ai/dsh` production closure therefore includes neither product provider, the Claude Agent SDK, nor the Codex wrapper and platform payloads. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The patch gates both shell stacks by platform on its own rows: `bash-sandbox`/`tool-bash` carry `disabled: !!js process.platform === 'win32'` (bash has no Windows runner), and their twins `pwsh-sandbox`/`tool-pwsh` mount on win32 only with the inverted expression — one shared patch file, exactly one shell stack per host. The permission surface stays exactly as on POSIX: `sandbox`/`sandbox-policy` enforce the file-effect policy through the Windows ACL restricted-token runner (the win32 chain of `dsh-sandbox-local` → `@deepseek-ai/dsh-sandbox-windows-acl`), the permission switcher and the approval service run unchanged, and `fs-sandbox` keeps fencing `ctx.fs` writes — mounting `dsh-fs-local` alongside it would double-register `ctx.fs` and fail the load. A Windows host that prefers the unconfined local pwsh executor or full access overrides these rows through its profile or home `cordis.patch.yml` (the bash-restore recipe must be complete: disable `pwsh-sandbox`/`tool-pwsh` AND re-enable `bash-sandbox`/`tool-bash` — both executor families register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts see the pwsh rows disabled.
diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md
index c2a07ec158..797968d362 100644
--- a/packages/bundle/base/README.zh.md
+++ b/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、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 可以仅在需要时安装 [Claude Code provider Bundle](../../subagent/subagent-claude-code/README.md),使用 Codex 的部署仍须显式挂载该 provider。因此,默认的 `@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,绝不通过代码。
+以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settings/credentials、遥测与核心 spawn/fork subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。可选的 Codex 与 Claude Code provider 不属于本包及其生产依赖闭包;Profile 仅在需要时安装任一[产品 provider Bundle](../../subagent/README.md)。因此,默认的 `@deepseek-ai/dsh` 生产依赖闭包既不包含任一产品 provider、Claude Agent SDK,也不包含 Codex wrapper 及其平台载荷。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。
patch 在自身上按平台门控两个 shell 栈:`bash-sandbox`/`tool-bash` 携带 `disabled: !!js process.platform === 'win32'`(bash 没有 Windows runner),它们的孪生行 `pwsh-sandbox`/`tool-pwsh` 以取反的表达式仅在 win32 挂载——同一份 patch 文件,每个宿主恰好挂载一个 shell 栈。权限面与 POSIX 完全一致:`sandbox`/`sandbox-policy` 通过 Windows ACL 受限令牌 runner(`dsh-sandbox-local` 的 win32 链 → `@deepseek-ai/dsh-sandbox-windows-acl`)执行文件效果策略,权限切换器与 approval 服务原样运行,`fs-sandbox` 继续围栏 `ctx.fs` 写入——在其旁再挂载 `dsh-fs-local` 会重复注册 `ctx.fs` 并在加载时失败。偏好不受沙盒约束的本地 pwsh 执行器或完整访问的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行(bash 恢复配方必须完整:禁用 `pwsh-sandbox`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器家族注册同一个 `bash` 服务,配方不完整会在加载时直接报错)。POSIX 主机看到的是被禁用的 pwsh 行。
diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml
index 24fefb0441..35671f6659 100644
--- a/packages/subagent/README.i18n.yaml
+++ b/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: aebf368b984dca3ac2e37d1bcdba26a82ce85196
-README.zh.md: fb9e223f916a8b07d3a4ae254fa61087de081a93
+README.md: c881a749e1c43b3044029ebf4b53ed650566a875
+README.zh.md: 92afbc88f893119ac230f7fd98b1c3c5d6e11f47
diff --git a/packages/subagent/README.md b/packages/subagent/README.md
index aebf368b98..c881a749e1 100644
--- a/packages/subagent/README.md
+++ b/packages/subagent/README.md
@@ -18,7 +18,7 @@ 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 Claude Code package is also an optional Profile Bundle. Install it with `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; the package registers only its dormant Host provider, while a copied Agent Preset separately grants the disabled tool template to new Sessions. Removing the package withdraws that provider on the next Profile start. The Codex package remains an explicitly mounted Host plugin and uses a host `codex` from `PATH`.
+The Codex and Claude Code packages are independent optional Profile Bundles. Install either or both with `dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; each package registers only its dormant Host provider, while a copied Agent Preset separately grants the disabled tool template to new Sessions. Removing one package withdraws only that provider and its private runtime closure on the next Profile start.
See the decisions for the [capability family](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), [continuable children](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md), and [control tools](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md).
diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md
index fb9e223f91..92afbc88f8 100644
--- a/packages/subagent/README.zh.md
+++ b/packages/subagent/README.zh.md
@@ -18,7 +18,7 @@
| [`tool-subagent-control/`](tool-subagent-control/README.md) | 向模型公开子级消息发送和列举操作 | 注册到 `ctx.tools` |
| [`tool-subagent-report/`](tool-subagent-report/README.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
-Claude Code 包也是一个可选的 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code` 安装后重启该 Profile;该包只注册休眠的 Host provider,而复制出的 Agent Preset 会单独把默认禁用的工具模板授予新 Session。移除该包后,下一次 Profile 启动会撤回对应 provider。Codex 包仍须作为 Host 插件显式挂载,并使用 `PATH` 中的宿主 `codex`。
+Codex 与 Claude Code 包是彼此独立的可选 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-codex @deepseek-ai/dsh-subagent-claude-code` 安装其中一个或两个包,再重启该 Profile;每个包只注册自己的休眠 Host provider,而复制出的 Agent Preset 会单独把默认禁用的工具模板授予新 Session。移除其中一个包后,下一次 Profile 启动只会撤回对应 provider 及其私有运行时闭包。
参见有关[能力家族](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)、[可继续执行的子级](../../.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md)和[控制工具](../../.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.md)的决策。
diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml
index da14b8ff30..50bfe3dd92 100644
--- a/packages/subagent/subagent-codex/README.i18n.yaml
+++ b/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: 848d170585710b682fa4ce331010fce7080de673
-README.zh.md: 34e9105e6a78bc16f16997c7df89d4f6412eb50c
+README.md: 7e8c6c00d3f777ab6478087cfea189658ce83bd5
+README.zh.md: 9a87d3505dc94bf807f27809c4fb53c5644c5052
diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md
index 848d170585..7e8c6c00d3 100644
--- a/packages/subagent/subagent-codex/README.md
+++ b/packages/subagent/subagent-codex/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-This package registers the fixed `codex` subagent provider. Each accepted run starts the official `codex app-server --stdio` command in the delegating Session's workspace, creates one ephemeral Codex thread, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
+This package registers the fixed `codex` subagent provider. Each accepted run starts the official package-local Codex wrapper with `app-server --stdio` in the delegating Session's workspace, creates one ephemeral Codex thread, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
## Start and ownership
@@ -25,27 +25,31 @@ The provider advertises no optional start-time capabilities and reports `inherit
| `env` | `{}` | Explicit child environment layered over the subprocess seam's credential-scrubbed parent environment. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
-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.
+Production resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` dependency and launches that JavaScript wrapper with the current Node executable. The wrapper selects the matching native platform payload; the provider neither inspects nor falls back to a host `codex` on `PATH`. Native Codex configuration and authentication remain authoritative through the parent cwd, `HOME`, and `CODEX_HOME`. The plugin does not select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed by the subprocess seam before the explicit `env` overlay is applied.
-Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-codex` and mount it once on the host plane; loading the provider starts no Codex process until a tool call. Full Agent Presets carry a matching product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
+This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; installation brings the official wrapper and one compatible native platform payload into that Profile, while the declared `cordis.patch.yml` layer registers only the dormant `codex` Host provider and starts no Codex process. Removing the package withdraws that provider and its private runtime closure on the next Profile start.
-The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider row, and enables the preset tool row instead of mounting duplicate Job services.
+```sh
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh --profile
+```
+
+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. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base Host and full presets already provide the generic Job registry and controls. The 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//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
+```
-- id: jobs
- name: '@deepseek-ai/dsh-jobs-local'
-
-- id: tool-jobs
- name: '@deepseek-ai/dsh-tool-jobs'
-
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
config:
provider: codex
toolName: subagent_codex
@@ -55,7 +59,9 @@ The standalone composition below shows the complete explicit capability. A Profi
## Product compatibility and evidence
-The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`; the npm package is a test-only dependency, and deployments still supply `codex` on `PATH`.
+The production wire intentionally implements only the app-server methods required by this one-shot contract. The runtime dependency and all six optional-dependency aliases are pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`. A normal install selects one payload for the current OS and CPU. For the current darwin-arm64 payload, `npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` reports 111,199,052 packed bytes and 274,777,843 unpacked bytes. That package contains native `codex`, `codex-code-mode-host`, `rg`, and `zsh` resources; other platforms may differ, and these values are disclosure rather than an installation threshold. The keyless real-product test drives the package wrapper against a loopback Responses fixture, observes the package-local argv, and proves wrapper and native descendants become quiescent.
+
+Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload makes the first delegation fail with the wrapper's native-payload startup error. The provider neither probes a host CLI nor retries with one.
## Model Experience
@@ -63,7 +69,7 @@ The production wire intentionally implements only the app-server methods require
#### What the model sees
-The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd, and its model, system instructions, tools, sandbox, and authentication come from the native Codex installation and configuration.
+The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from native Codex configuration, while the executable version comes from the Bundle's pinned platform payload.
#### Token effect
@@ -90,7 +96,8 @@ Append-only: foreground adds one result after the reusable parent prefix, while
## Known Limitations and Deferred Work
- **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
-- **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate.
+- **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, trust a project, or rewrite Codex settings; configuration and authentication failures surface as startup or run errors.
+- **The native platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first run; there is no host-CLI fallback.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package.
- **Product payload is final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md
index 34e9105e6a..9a87d3505d 100644
--- a/packages/subagent/subagent-codex/README.zh.md
+++ b/packages/subagent/subagent-codex/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-本包注册固定的 `codex` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中启动官方 `codex app-server --stdio` 命令,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
+本包注册固定的 `codex` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中使用 `app-server --stdio` 启动官方包内 Codex wrapper,创建一个临时 Codex 线程,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
## 启动与所有权
@@ -25,27 +25,31 @@
| `env` | `{}` | 显式指定的子进程环境,叠加在由子进程 seam 清除凭证后的父环境之上。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
-生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。
+生产环境会解析锁定的 `@openai/codex@0.147.0` 依赖所声明的 `codex` bin,并使用当前 Node 可执行文件启动该 JavaScript wrapper。Wrapper 会选择匹配的原生平台载荷;提供方既不检查也不回退 `PATH` 中的宿主 `codex`。父会话 cwd、`HOME` 与 `CODEX_HOME` 继续让原生 Codex 配置和身份验证保持权威。本插件不选择模型、不创建产品主目录、不执行登录,也不探测账户。子进程 seam 会先移除具有凭证特征的环境变量,再应用显式 `env` 覆盖。
-生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-codex`,并在 host plane(宿主平面)挂载一次;加载提供方本身不会在工具调用前启动 Codex 进程。完整 Agent Preset 携带对应的产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
+本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;安装会把官方 wrapper 与一个兼容的原生平台载荷带入该 Profile,而包所声明的 `cordis.patch.yml` 层只注册休眠的 `codex` Host provider,不会启动 Codex 进程。移除该包后,下一次 Profile 启动会撤回这一 provider 及其私有运行时闭包。
-下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 行,只新增产品提供方行并启用 preset 工具行,禁止重复挂载 Job 服务。
+```sh
+dsh plugin --profile add @deepseek-ai/dsh-subagent-codex
+dsh plugin --profile remove @deepseek-ai/dsh-subagent-codex
+dsh --profile
+```
+
+安装决定 Host 可用性,而不是模型权限。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的新 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base Host 与完整 preset 已提供通用 Job 注册表和控制工具。Profile 自己的 patch 可以替换 Bundle 行的完整 `config`,而自定义 Host 组合仍可直接挂载本包。
```yaml
+# $DSH_HOME/profiles//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
+```
-- id: jobs
- name: '@deepseek-ai/dsh-jobs-local'
-
-- id: tool-jobs
- name: '@deepseek-ai/dsh-tool-jobs'
-
+```yaml
+# A copied Agent Preset; remove `disabled` to grant this tool.
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
config:
provider: codex
toolName: subagent_codex
@@ -55,7 +59,9 @@
## 产品兼容性与证据
-生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.147.0` / `codex-cli 0.147.0`;该 NPM 包仅作为测试依赖,部署环境仍需通过 `PATH` 提供 `codex`。
+生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。运行时依赖与六个 optional-dependency alias 均锁定到 `@openai/codex@0.147.0` / `codex-cli 0.147.0`。普通安装会按当前操作系统与 CPU 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` 报告压缩包为 111,199,052 字节、解包后为 274,777,843 字节。该包包含原生 `codex`、`codex-code-mode-host`、`rg` 与 `zsh` 资源;其他平台可能不同,这些数值只用于披露而不是安装阈值。无密钥真实产品测试会驱动包内 wrapper 连接回环 Responses fixture,观测包内 argv,并证明 wrapper 与原生后代进程完全停稳。
+
+如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,第一次委派会以 wrapper 的原生载荷启动错误失败。提供方既不会探测宿主 CLI,也不会用它重试。
## 模型体验
@@ -63,7 +69,7 @@
#### 模型看到的内容
-Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Codex 安装与配置。
+Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自原生 Codex 配置,可执行版本则来自 Bundle 锁定的平台载荷。
#### 对 token 的影响
@@ -90,7 +96,8 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
## 已知限制与后续工作
- **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。
-- **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
+- **身份验证与账户状态仍由原生机制管理**:Bundle 会提供 CLI,但不会创建账户、登录、信任项目或改写 Codex 设置;配置与身份验证失败会呈现为启动错误或运行错误。
+- **委派时必须存在原生平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次运行时失败;不会回退到宿主 CLI。
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。
- **产品载荷仅包含最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时。
diff --git a/packages/subagent/subagent-codex/cordis.patch.yml b/packages/subagent/subagent-codex/cordis.patch.yml
new file mode 100644
index 0000000000..fb64fdcb9d
--- /dev/null
+++ b/packages/subagent/subagent-codex/cordis.patch.yml
@@ -0,0 +1,5 @@
+# Optional Profile Bundle: register the Codex provider on the Host plane only.
+# Agent Presets grant the model-facing tool independently.
+- insert:
+ - id: subagent-codex
+ name: '@deepseek-ai/dsh-subagent-codex'
diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json
index 0256ee8e21..1d948eb631 100644
--- a/packages/subagent/subagent-codex/package.json
+++ b/packages/subagent/subagent-codex/package.json
@@ -28,13 +28,18 @@
"files": [
"lib/index.js",
"lib/invariant.js",
+ "cordis.patch.yml",
"lib/types/**/*.d.ts"
],
"license": "MIT",
+ "dsh": {
+ "bundle": {
+ "patch": "./cordis.patch.yml"
+ }
+ },
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
- "@deepseek-ai/dsh-sdk-protocol": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
@@ -42,7 +47,9 @@
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^"
+ "@deepseek-ai/dsh-sdk-protocol": "workspace:^",
+ "@deepseek-ai/schemastery": "workspace:^",
+ "@openai/codex": "0.147.0"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
@@ -56,7 +63,6 @@
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
- "@openai/codex": "0.147.0",
"@deepseek-ai/cordis": "workspace:^"
}
}
diff --git a/packages/subagent/subagent-codex/src/index.ts b/packages/subagent/subagent-codex/src/index.ts
index 3b1bbec799..ec9ff34945 100644
--- a/packages/subagent/subagent-codex/src/index.ts
+++ b/packages/subagent/subagent-codex/src/index.ts
@@ -1,7 +1,7 @@
/**
* Fixed Codex one-shot subagent provider. Every accepted run starts a fresh
- * official `codex app-server --stdio` process in the delegating Session's
- * workspace and publishes only after an ephemeral thread exists.
+ * official package-local Codex wrapper with `app-server --stdio` in the
+ * delegating Session's workspace and publishes only after an ephemeral thread exists.
*
* @module @deepseek-ai/dsh-subagent-codex
*/
diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts
index ebce244f3b..d497a4f16c 100644
--- a/packages/subagent/subagent-codex/src/run.ts
+++ b/packages/subagent/subagent-codex/src/run.ts
@@ -8,6 +8,9 @@
*/
import { randomUUID } from 'node:crypto'
+import { readFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { dirname, resolve } from 'node:path'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import {
@@ -24,21 +27,46 @@ import { CodexAppServerWire } from './wire.ts'
/** Default POSIX grace between subprocess termination tiers. */
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
+interface CodexPackageManifest {
+ readonly bin?: string | Readonly>
+}
+
/**
- * Resolve the fixed app-server command for a platform.
- *
- * Windows npm and pnpm installs expose `codex.cmd`, which requires `cmd.exe`;
- * the argv is constant so no task or configuration text enters the
- * shell boundary.
- * @param platform - host platform used to select the executable boundary.
- * @returns argv for the fixed Codex app-server command.
+ * Resolve the official package's declared `codex` bin relative to its manifest.
+ * @param packageJsonPath - absolute path to the official package manifest.
+ * @param manifest - parsed manifest carrying the declared bin entry.
+ * @returns absolute path to the package-local JavaScript wrapper.
*/
-export function codexAppServerArgv(
- platform: NodeJS.Platform = process.platform,
-): string[] {
- return platform === 'win32'
- ? ['cmd.exe', '/d', '/s', '/c', 'codex', 'app-server', '--stdio']
- : ['codex', 'app-server', '--stdio']
+export function codexPackageBinPath(
+ packageJsonPath: string,
+ manifest: CodexPackageManifest,
+): string {
+ const declared = typeof manifest.bin === 'string'
+ ? manifest.bin
+ : manifest.bin?.codex
+ if (declared === undefined || declared.length === 0) {
+ throw new Error('@openai/codex does not declare its codex bin')
+ }
+ return resolve(dirname(packageJsonPath), declared)
+}
+
+const codexPackageJsonPath = createRequire(import.meta.url).resolve('@openai/codex/package.json')
+const codexPackageManifest = JSON.parse(
+ readFileSync(codexPackageJsonPath, 'utf8'),
+) as CodexPackageManifest
+
+/** Absolute package-local JavaScript wrapper selected by the package manifest. */
+export const CODEX_PACKAGE_BIN = codexPackageBinPath(
+ codexPackageJsonPath,
+ codexPackageManifest,
+)
+
+/**
+ * Fixed package-local app-server command, independent of the host `PATH`.
+ * @returns Node, the official wrapper, and the fixed app-server arguments.
+ */
+export function codexAppServerArgv(): string[] {
+ return [process.execPath, CODEX_PACKAGE_BIN, 'app-server', '--stdio']
}
/** Fully resolved inputs for one Codex app-server run. */
diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
index 8e265c3207..b72144e299 100644
--- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts
+++ b/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.
diff --git a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
index 075b6048f2..2e876134a3 100644
--- a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
+++ b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
@@ -8,7 +8,7 @@ import {
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
-import { delimiter, join, resolve } from 'node:path'
+import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
@@ -18,6 +18,7 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
+import { CODEX_PACKAGE_BIN } from '../src/run.ts'
import {
startDeepSeekResponsesBridge,
type DeepSeekResponsesBridge,
@@ -25,7 +26,6 @@ import {
const execFileAsync = promisify(execFile)
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
-const codexBinDir = join(packageRoot, 'node_modules', '.bin')
const codexPackage = JSON.parse(readFileSync(
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
'utf8',
@@ -88,7 +88,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
CODEX_HOME: codexHome,
HOME: root,
XDG_CONFIG_HOME: join(root, 'xdg-config'),
- PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
+ PATH: root,
HTTP_PROXY: '',
HTTPS_PROXY: '',
ALL_PROXY: '',
@@ -106,7 +106,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
return handle
})
await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
- const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], {
+ const version = await execFileAsync(process.execPath, [CODEX_PACKAGE_BIN, '--version'], {
env: { ...process.env, ...env },
})
expect(codexPackage.version).toBe('0.147.0')
diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts
index 551d6db765..8b1181a743 100644
--- a/packages/subagent/subagent-codex/tests/real-product.spec.ts
+++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts
@@ -1,5 +1,6 @@
import { execFile } from 'node:child_process'
import {
+ cpSync,
existsSync,
mkdirSync,
mkdtempSync,
@@ -8,16 +9,17 @@ import {
} from 'node:fs'
import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
-import { delimiter, join, resolve } from 'node:path'
+import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
-import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
+import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
+import { CODEX_PACKAGE_BIN } from '../src/run.ts'
import {
startResponsesFixture,
type ResponsesBehavior,
@@ -27,7 +29,8 @@ import {
const execFileAsync = promisify(execFile)
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
-const codexEntry = join(packageRoot, 'node_modules', '@openai', 'codex', 'bin', 'codex.js')
+const codexEntry = CODEX_PACKAGE_BIN
+const codexPackageRoot = dirname(dirname(codexEntry))
const codexPackage = JSON.parse(readFileSync(
join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
'utf8',
@@ -48,6 +51,7 @@ afterEach(async () => {
interface RealHarness {
readonly ctx: Context
readonly handles: SubprocessHandle[]
+ readonly spawnSpecs: SubprocessSpawnSpec[]
readonly parent: Agent
readonly env: Record
readonly workspace: string
@@ -89,7 +93,7 @@ async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
CODEX_HOME: codexHome,
HOME: root,
XDG_CONFIG_HOME: join(root, 'xdg'),
- PATH: `${codexBinDir}${delimiter}${process.env.PATH ?? ''}`,
+ PATH: root,
HTTP_PROXY: '',
HTTPS_PROXY: '',
ALL_PROXY: '',
@@ -100,8 +104,10 @@ async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
+ const spawnSpecs: SubprocessSpawnSpec[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
+ spawnSpecs.push(spec)
const handle = spawn(spec)
handles.push(handle)
return handle
@@ -111,7 +117,7 @@ async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
id: 'real-parent',
session: { header: { cwd: workspace } },
} as unknown as Agent
- return { harness: { ctx, handles, parent, env, workspace }, fixture }
+ return { harness: { ctx, handles, spawnSpecs, parent, env, workspace }, fixture }
}
async function expectQuiescent(handles: readonly SubprocessHandle[]): Promise {
@@ -164,6 +170,13 @@ describe('real @openai/codex 0.147.0 product', () => {
})
await run.dispose()
+ expect(harness.spawnSpecs[0]?.argv).toEqual([
+ process.execPath,
+ codexEntry,
+ 'app-server',
+ '--stdio',
+ ])
+
expect(fixture.requests).toHaveLength(1)
const recorded = fixture.requests[0]!
expect(recorded.method).toBe('POST')
@@ -173,6 +186,24 @@ describe('real @openai/codex 0.147.0 product', () => {
await expectQuiescent(harness.handles)
}, 60_000)
+ it('fails a missing platform payload without falling back to a host codex', async () => {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-codex-missing-payload-'))
+ roots.push(root)
+ const isolatedPackage = join(root, 'node_modules', '@openai', 'codex')
+ mkdirSync(dirname(isolatedPackage), { recursive: true })
+ cpSync(codexPackageRoot, isolatedPackage, { recursive: true, dereference: true })
+ const isolatedEntry = join(isolatedPackage, 'bin', 'codex.js')
+
+ await expect(execFileAsync(process.execPath, [isolatedEntry, '--version'], {
+ env: {
+ PATH: codexBinDir,
+ ...process.platform === 'win32' && process.env.SystemRoot !== undefined
+ ? { SystemRoot: process.env.SystemRoot }
+ : {},
+ },
+ })).rejects.toThrow('Missing optional dependency')
+ }, 30_000)
+
it('cancels a real app-server command approval without executing the command', async () => {
const command = process.platform === 'win32'
? 'cmd /c type nul > approval-side-effect'
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 37b2e9ff0b..a60ed2f759 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/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'
@@ -15,7 +19,9 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
+ CODEX_PACKAGE_BIN,
codexAppServerArgv,
+ codexPackageBinPath,
DEFAULT_DISPOSE_GRACE_MS,
disposeCodexChild,
startCodexRun,
@@ -26,6 +32,16 @@ import { CodexAppServerWire } from '../src/wire.ts'
type JsonObject = Record
+const CODEX_VERSION = '0.147.0'
+const CODEX_PLATFORM_PACKAGES = [
+ '@openai/codex-darwin-arm64',
+ '@openai/codex-darwin-x64',
+ '@openai/codex-linux-arm64',
+ '@openai/codex-linux-x64',
+ '@openai/codex-win32-arm64',
+ '@openai/codex-win32-x64',
+] as const
+
const fakeParent = {
id: 'parent',
session: { header: { cwd: process.cwd() } },
@@ -260,17 +276,76 @@ function turnCompleted(
}
describe('task admission and package contracts', () => {
- it('resolves the fixed app-server command through the Windows npm shim boundary', () => {
- expect(codexAppServerArgv('win32')).toEqual([
- 'cmd.exe',
- '/d',
- '/s',
- '/c',
- 'codex',
+ it('ships one independently installable provider-only Bundle patch', () => {
+ const root = fileURLToPath(new URL('..', import.meta.url))
+ const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
+ dependencies?: Record
+ files?: string[]
+ dsh?: { bundle?: { patch?: string } }
+ }
+ expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
+ expect(manifest.files).toContain('cordis.patch.yml')
+ expect(manifest.dependencies).toHaveProperty(
+ '@deepseek-ai/dsh-sdk-protocol',
+ 'workspace:^',
+ )
+ expect(manifest.dependencies).toHaveProperty('@openai/codex', CODEX_VERSION)
+ expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-claude-code')
+
+ const codexPackageJson = fileURLToPath(import.meta.resolve('@openai/codex/package.json'))
+ const codexManifest = JSON.parse(readFileSync(codexPackageJson, 'utf8')) as {
+ version: string
+ bin: Record
+ optionalDependencies: Record
+ }
+ expect(codexManifest.version).toBe(CODEX_VERSION)
+ expect(codexManifest.bin).toEqual({ codex: 'bin/codex.js' })
+ expect(codexManifest.optionalDependencies).toEqual(Object.fromEntries(
+ CODEX_PLATFORM_PACKAGES.map(packageName => [
+ packageName,
+ `npm:@openai/codex@${CODEX_VERSION}-${packageName.slice('@openai/codex-'.length)}`,
+ ]),
+ ))
+ expect(CODEX_PACKAGE_BIN).toBe(codexPackageBinPath(codexPackageJson, codexManifest))
+
+ const lockfile = readFileSync(resolve(root, '../../../pnpm-lock.yaml'), 'utf8')
+ for (const packageName of CODEX_PLATFORM_PACKAGES) {
+ const suffix = packageName.slice('@openai/codex-'.length)
+ expect(lockfile).toContain(` '@openai/codex@${CODEX_VERSION}-${suffix}':`)
+ expect(lockfile).toContain(
+ ` '${packageName}': '@openai/codex@${CODEX_VERSION}-${suffix}'`,
+ )
+ }
+
+ const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'))
+ const rows = Array.isArray(parsed)
+ ? (parsed as Array<{ insert?: Array<{ id?: string; name?: string }> }>).flatMap(entry => entry.insert ?? [])
+ : []
+ expect(rows).toEqual([{
+ id: 'subagent-codex',
+ name: '@deepseek-ai/dsh-subagent-codex',
+ }])
+ expect(JSON.stringify(rows)).not.toContain('tool-subagent')
+ })
+
+ it('uses only the official package-declared wrapper for app-server', () => {
+ expect(codexPackageBinPath(
+ '/package/node_modules/@openai/codex/package.json',
+ { bin: 'bin/codex.js' },
+ )).toBe('/package/node_modules/@openai/codex/bin/codex.js')
+ expect(codexPackageBinPath(
+ '/package/node_modules/@openai/codex/package.json',
+ { bin: { codex: 'bin/codex.js' } },
+ )).toBe('/package/node_modules/@openai/codex/bin/codex.js')
+ expect(() => codexPackageBinPath('/package/package.json', {}))
+ .toThrow('does not declare its codex bin')
+ expect(codexAppServerArgv()).toEqual([
+ process.execPath,
+ CODEX_PACKAGE_BIN,
'app-server',
'--stdio',
])
- expect(codexAppServerArgv('linux')).toEqual(['codex', 'app-server', '--stdio'])
+ expect(codexAppServerArgv()).not.toContain('codex')
})
it('accepts one or more text blocks and rejects empty or non-text tasks', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0b13703008..62c10d31ec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7114,9 +7114,15 @@ importers:
packages/subagent/subagent-codex:
dependencies:
+ '@deepseek-ai/dsh-sdk-protocol':
+ specifier: workspace:^
+ version: link:../../sdk/protocol
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
+ '@openai/codex':
+ specifier: 0.147.0
+ version: 0.147.0
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -7136,9 +7142,6 @@ importers:
'@deepseek-ai/dsh-loader-smoke':
specifier: workspace:^
version: link:../../test-support/loader-smoke
- '@deepseek-ai/dsh-sdk-protocol':
- specifier: workspace:^
- version: link:../../sdk/protocol
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
@@ -7154,9 +7157,6 @@ importers:
'@deepseek-ai/dsh-timeout':
specifier: workspace:^
version: link:../../util/timeout
- '@openai/codex':
- specifier: 0.147.0
- version: 0.147.0
packages/subagent/subagent-dsh-sdk:
dependencies:
diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts
index 479a2f13b1..8438a5edb1 100644
--- a/scripts/gen-third-party-notices.spec.ts
+++ b/scripts/gen-third-party-notices.spec.ts
@@ -4,7 +4,9 @@ import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import {
CLAUDE_AGENT_SDK_PACKAGE,
+ CODEX_PACKAGE,
claudeDistributionFromManifest,
+ codexDistributionFromManifest,
collectPythonDependencies,
isOwnerAuthorizedRuntime,
isPermissive,
@@ -331,6 +333,53 @@ describe('official Claude distribution authorization', () => {
})
})
+describe('official Codex platform payloads', () => {
+ it('derives versioned packages from the wrapper aliases', () => {
+ expect(codexDistributionFromManifest({
+ name: CODEX_PACKAGE,
+ version: '9.8.7',
+ optionalDependencies: {
+ '@openai/codex-linux-x64': 'npm:@openai/codex@9.8.7-linux-x64',
+ '@openai/codex-darwin-arm64': 'npm:@openai/codex@9.8.7-darwin-arm64',
+ },
+ })).toEqual({
+ wrapperVersion: '9.8.7',
+ payloads: [
+ { alias: '@openai/codex-darwin-arm64', version: '9.8.7-darwin-arm64' },
+ { alias: '@openai/codex-linux-x64', version: '9.8.7-linux-x64' },
+ ],
+ })
+ })
+
+ it('rejects a wrong identity, missing payloads, and non-official aliases', () => {
+ expect(() => codexDistributionFromManifest({
+ name: '@openai/unrelated',
+ version: '1.0.0',
+ optionalDependencies: {
+ '@openai/codex-linux-x64': 'npm:@openai/codex@1.0.0-linux-x64',
+ },
+ })).toThrow(`expected ${CODEX_PACKAGE} manifest`)
+ expect(() => codexDistributionFromManifest({
+ name: CODEX_PACKAGE,
+ version: '1.0.0',
+ })).toThrow('declares no optional platform payloads')
+ expect(() => codexDistributionFromManifest({
+ name: CODEX_PACKAGE,
+ version: '1.0.0',
+ optionalDependencies: {
+ '@openai/unrelated': 'npm:@openai/codex@1.0.0-linux-x64',
+ },
+ })).toThrow('outside its platform alias namespace')
+ expect(() => codexDistributionFromManifest({
+ name: CODEX_PACKAGE,
+ version: '1.0.0',
+ optionalDependencies: {
+ '@openai/codex-linux-x64': '1.0.0',
+ },
+ })).toThrow('does not alias an official versioned payload')
+ })
+})
+
describe('manifestPatterns', () => {
it('derives globs from the declared members, so a new member area is read', () => {
expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([
diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts
index f2411f5eff..f76ae1d358 100644
--- a/scripts/gen-third-party-notices.ts
+++ b/scripts/gen-third-party-notices.ts
@@ -50,6 +50,9 @@ const FIRST_PARTY = new Set([
export const CLAUDE_AGENT_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk'
const CLAUDE_PLATFORM_PACKAGE_PREFIX = `${CLAUDE_AGENT_SDK_PACKAGE}-`
const CLAUDE_PLATFORM_DECLARED_LICENSE = 'SEE LICENSE IN LICENSE.md'
+export const CODEX_PACKAGE = '@openai/codex'
+const CODEX_PLATFORM_ALIAS_PREFIX = `${CODEX_PACKAGE}-`
+const CODEX_DECLARED_LICENSE = 'Apache-2.0'
/**
* Whether a non-permissive runtime declaration has an identity-scoped owner
@@ -194,6 +197,18 @@ export interface ClaudeDistribution {
readonly payloads: ClaudePlatformPayload[]
}
+/** One optional-dependency alias for an official Codex platform payload. */
+export interface CodexPlatformPayload {
+ readonly alias: string
+ readonly version: string
+}
+
+/** Current Codex wrapper and platform payload facts from the official manifest. */
+export interface CodexDistribution {
+ readonly wrapperVersion: string
+ readonly payloads: CodexPlatformPayload[]
+}
+
function requiredManifestString(
value: string | undefined,
field: string,
@@ -243,6 +258,40 @@ export function claudeDistributionFromManifest(
return { sdkVersion, claudeCodeVersion, payloads }
}
+/** Derive official Codex platform aliases and published package versions. */
+export function codexDistributionFromManifest(
+ manifest: VirtualManifest,
+): CodexDistribution {
+ if (manifest.name !== CODEX_PACKAGE) {
+ throw new Error(
+ `gen-third-party-notices: expected ${CODEX_PACKAGE} manifest, got ${JSON.stringify(manifest.name)}.`,
+ )
+ }
+ const wrapperVersion = manifest.version
+ if (wrapperVersion === undefined || wrapperVersion.length === 0) {
+ throw new Error(`gen-third-party-notices: ${CODEX_PACKAGE} has no version.`)
+ }
+ const entries = Object.entries(manifest.optionalDependencies ?? {})
+ if (entries.length === 0) {
+ throw new Error(`gen-third-party-notices: ${CODEX_PACKAGE} declares no optional platform payloads.`)
+ }
+ const payloads = entries.map(([alias, spec]) => {
+ if (!alias.startsWith(CODEX_PLATFORM_ALIAS_PREFIX)) {
+ throw new Error(
+ `gen-third-party-notices: ${CODEX_PACKAGE} optional dependency ${alias} is outside its platform alias namespace.`,
+ )
+ }
+ const prefix = `npm:${CODEX_PACKAGE}@`
+ if (!spec.startsWith(prefix) || spec.length === prefix.length) {
+ throw new Error(
+ `gen-third-party-notices: ${CODEX_PACKAGE} optional dependency ${alias} does not alias an official versioned payload.`,
+ )
+ }
+ return { alias, version: spec.slice(prefix.length) }
+ }).sort((left, right) => left.alias.localeCompare(right.alias))
+ return { wrapperVersion, payloads }
+}
+
/**
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
* matches ordinary `@scope+name@version` directory names; pnpm 11 truncates
@@ -333,6 +382,54 @@ function collectClaudeDistribution(): ClaudeDistribution {
return distribution
}
+/** Resolve an installed versioned Codex payload manifest from either pnpm store. */
+function installedCodexPayload(version: string): VirtualManifest | undefined {
+ for (const store of ['node_modules', 'native/landlock-run/node_modules']) {
+ const candidate = resolve(
+ root,
+ store,
+ '.pnpm',
+ `${CODEX_PACKAGE.replace('/', '+')}@${version}`,
+ 'node_modules',
+ CODEX_PACKAGE,
+ 'package.json',
+ )
+ if (existsSync(candidate)) {
+ return JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest
+ }
+ }
+ return undefined
+}
+
+function collectCodexDistribution(): CodexDistribution {
+ const manifest = installedManifest(CODEX_PACKAGE)
+ if (manifest === undefined) {
+ throw new Error(`gen-third-party-notices: cannot resolve ${CODEX_PACKAGE}; run \`pnpm install\`.`)
+ }
+ const distribution = codexDistributionFromManifest(manifest)
+ let installedPayloads = 0
+ for (const payload of distribution.payloads) {
+ const installed = installedCodexPayload(payload.version)
+ if (installed === undefined) continue
+ installedPayloads += 1
+ if (
+ installed.name !== CODEX_PACKAGE
+ || installed.version !== payload.version
+ || installed.license !== CODEX_DECLARED_LICENSE
+ ) {
+ throw new Error(
+ `gen-third-party-notices: installed ${payload.alias} does not match its official ${CODEX_PACKAGE}@${payload.version} payload and ${CODEX_DECLARED_LICENSE} license.`,
+ )
+ }
+ }
+ if (installedPayloads === 0) {
+ throw new Error(
+ 'gen-third-party-notices: no Codex platform payload is installed; install optional dependencies before regenerating.',
+ )
+ }
+ return distribution
+}
+
/** Normalize a manifest repository/homepage value to a browsable https URL. */
function normalizeRepo(raw: string | undefined): string | undefined {
if (raw === undefined || raw === '') return undefined
@@ -656,6 +753,24 @@ ${rows.join('\n')}
`
}
+function renderCodexDistribution(
+ distribution: CodexDistribution | undefined,
+): string {
+ if (distribution === undefined) return ''
+ const rows = distribution.payloads.map(payload => (
+ `| \`${payload.alias}\` | [\`${CODEX_PACKAGE}\`](https://www.npmjs.com/package/${CODEX_PACKAGE}/v/${payload.version}) | ${payload.version} | ${CODEX_DECLARED_LICENSE} |`
+ ))
+ return `
+## Official Codex platform payloads
+
+The installed \`${CODEX_PACKAGE}\` wrapper ${distribution.wrapperVersion} declares the following optional-dependency aliases. Every alias resolves to an official platform-specific \`${CODEX_PACKAGE}\` version that carries the native Codex CLI and its bundled resources; the declared license is verified against the payload installed for the current host.
+
+| Optional dependency alias | Published package | Version | Declared license |
+| --- | --- | --- | --- |
+${rows.join('\n')}
+`
+}
+
/**
* Render the complete notices document.
* @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
@@ -673,6 +788,9 @@ export function render(): string {
)
? collectClaudeDistribution()
: undefined
+ const codexDistribution = runtimeDeps.some(dep => dep.name === CODEX_PACKAGE)
+ ? collectCodexDistribution()
+ : undefined
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
// A copyleft license reaching a shipped surface is a distribution decision,
@@ -693,7 +811,7 @@ export function render(): string {
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
-This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
+This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code and Codex platform payload closures. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
@@ -715,6 +833,7 @@ pnpm applies local patches to the following packages at install time, so shipped
${patchedLines.join('\n')}
${renderClaudeDistribution(claudeDistribution)}
+${renderCodexDistribution(codexDistribution)}
## Development-only npm dependencies
diff --git a/scripts/verify-cordis-config.spec.ts b/scripts/verify-cordis-config.spec.ts
index 07f655823b..f63031e46c 100644
--- a/scripts/verify-cordis-config.spec.ts
+++ b/scripts/verify-cordis-config.spec.ts
@@ -4,10 +4,9 @@
* metadata field must stay static, and a disabled expression must parse.
*/
-import { globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { mkdirSync, mkdtempSync, 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 {
bundleManifestPaths,
@@ -15,36 +14,6 @@ import {
metadataExpressionErrors,
} from './verify-cordis-config.ts'
-interface WorkspaceManifest {
- name?: string
- dependencies?: Record
- optionalDependencies?: Record
- peerDependencies?: Record
-}
-
-const repoRoot = fileURLToPath(new URL('..', import.meta.url))
-
-function productionClosure(entry: string): Set {
- const manifests = new Map()
- 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()
- 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', () => {
const problems = metadataExpressionErrors(
@@ -116,15 +85,4 @@ describe('workspace Bundle discovery and product dependency closures', () => {
`${file}: @deepseek-ai/dsh-missing-plugin must be declared in ${manifestPath} dependencies`,
])
})
-
- it('keeps the default and optional Claude Code closure 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 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')
- })
})
From b6c52c82bbe47a63981e889d08c268d8851e52d3 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 03:52:23 +0800
Subject: [PATCH 011/134] fix(infra): resolve Codex notices from wrapper
---
scripts/gen-third-party-notices.spec.ts | 40 +++++++++
scripts/gen-third-party-notices.ts | 104 ++++++++++++++----------
2 files changed, 101 insertions(+), 43 deletions(-)
diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts
index 8438a5edb1..bd78e6f0fc 100644
--- a/scripts/gen-third-party-notices.spec.ts
+++ b/scripts/gen-third-party-notices.spec.ts
@@ -7,6 +7,7 @@ import {
CODEX_PACKAGE,
claudeDistributionFromManifest,
codexDistributionFromManifest,
+ codexDistributionFromInstalledPackage,
collectPythonDependencies,
isOwnerAuthorizedRuntime,
isPermissive,
@@ -378,6 +379,45 @@ describe('official Codex platform payloads', () => {
},
})).toThrow('does not alias an official versioned payload')
})
+
+ it('resolves installed payload aliases from the wrapper package', () => {
+ const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-notices-codex-wrapper-'))
+ try {
+ const wrapperPath = join(
+ fixtureRoot,
+ 'node_modules/@openai/codex/package.json',
+ )
+ const payloadPath = join(
+ fixtureRoot,
+ 'node_modules/@openai/codex-darwin-arm64/package.json',
+ )
+ mkdirSync(resolve(wrapperPath, '..'), { recursive: true })
+ mkdirSync(resolve(payloadPath, '..'), { recursive: true })
+ writeFileSync(wrapperPath, JSON.stringify({
+ name: CODEX_PACKAGE,
+ version: '9.8.7',
+ optionalDependencies: {
+ '@openai/codex-darwin-arm64': 'npm:@openai/codex@9.8.7-darwin-arm64',
+ '@openai/codex-linux-x64': 'npm:@openai/codex@9.8.7-linux-x64',
+ },
+ }))
+ writeFileSync(payloadPath, JSON.stringify({
+ name: CODEX_PACKAGE,
+ version: '9.8.7-darwin-arm64',
+ license: 'Apache-2.0',
+ }))
+
+ expect(codexDistributionFromInstalledPackage(wrapperPath)).toEqual({
+ wrapperVersion: '9.8.7',
+ payloads: [
+ { alias: '@openai/codex-darwin-arm64', version: '9.8.7-darwin-arm64' },
+ { alias: '@openai/codex-linux-x64', version: '9.8.7-linux-x64' },
+ ],
+ })
+ } finally {
+ rmSync(fixtureRoot, { recursive: true, force: true })
+ }
+ })
})
describe('manifestPatterns', () => {
diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts
index f76ae1d358..314c9b1b28 100644
--- a/scripts/gen-third-party-notices.ts
+++ b/scripts/gen-third-party-notices.ts
@@ -9,6 +9,7 @@
*/
import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
@@ -292,6 +293,57 @@ export function codexDistributionFromManifest(
return { wrapperVersion, payloads }
}
+function requireManifest(
+ requireFrom: NodeJS.Require,
+ name: string,
+): VirtualManifest | undefined {
+ let packageJsonPath: string
+ try {
+ packageJsonPath = requireFrom.resolve(`${name}/package.json`)
+ } catch (error: unknown) {
+ if (error instanceof Error && 'code' in error && error.code === 'MODULE_NOT_FOUND') {
+ return undefined
+ }
+ throw error
+ }
+ return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as VirtualManifest
+}
+
+/**
+ * Derive and verify the Codex distribution from the wrapper package's own
+ * Node resolution context.
+ * @param packageJsonPath - absolute manifest path for the installed wrapper.
+ * @returns the wrapper version and all declared platform aliases.
+ */
+export function codexDistributionFromInstalledPackage(
+ packageJsonPath: string,
+): CodexDistribution {
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as VirtualManifest
+ const distribution = codexDistributionFromManifest(manifest)
+ const requireFromWrapper = createRequire(packageJsonPath)
+ let installedPayloads = 0
+ for (const payload of distribution.payloads) {
+ const installed = requireManifest(requireFromWrapper, payload.alias)
+ if (installed === undefined) continue
+ installedPayloads += 1
+ if (
+ installed.name !== CODEX_PACKAGE
+ || installed.version !== payload.version
+ || installed.license !== CODEX_DECLARED_LICENSE
+ ) {
+ throw new Error(
+ `gen-third-party-notices: installed ${payload.alias} does not match its official ${CODEX_PACKAGE}@${payload.version} payload and ${CODEX_DECLARED_LICENSE} license.`,
+ )
+ }
+ }
+ if (installedPayloads === 0) {
+ throw new Error(
+ 'gen-third-party-notices: no Codex platform payload is installed; install optional dependencies before regenerating.',
+ )
+ }
+ return distribution
+}
+
/**
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
* matches ordinary `@scope+name@version` directory names; pnpm 11 truncates
@@ -382,52 +434,18 @@ function collectClaudeDistribution(): ClaudeDistribution {
return distribution
}
-/** Resolve an installed versioned Codex payload manifest from either pnpm store. */
-function installedCodexPayload(version: string): VirtualManifest | undefined {
- for (const store of ['node_modules', 'native/landlock-run/node_modules']) {
- const candidate = resolve(
- root,
- store,
- '.pnpm',
- `${CODEX_PACKAGE.replace('/', '+')}@${version}`,
- 'node_modules',
- CODEX_PACKAGE,
- 'package.json',
- )
- if (existsSync(candidate)) {
- return JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest
- }
- }
- return undefined
-}
-
function collectCodexDistribution(): CodexDistribution {
- const manifest = installedManifest(CODEX_PACKAGE)
- if (manifest === undefined) {
+ const requireFromProvider = createRequire(resolve(
+ root,
+ 'packages/subagent/subagent-codex/package.json',
+ ))
+ let packageJsonPath: string
+ try {
+ packageJsonPath = requireFromProvider.resolve(`${CODEX_PACKAGE}/package.json`)
+ } catch {
throw new Error(`gen-third-party-notices: cannot resolve ${CODEX_PACKAGE}; run \`pnpm install\`.`)
}
- const distribution = codexDistributionFromManifest(manifest)
- let installedPayloads = 0
- for (const payload of distribution.payloads) {
- const installed = installedCodexPayload(payload.version)
- if (installed === undefined) continue
- installedPayloads += 1
- if (
- installed.name !== CODEX_PACKAGE
- || installed.version !== payload.version
- || installed.license !== CODEX_DECLARED_LICENSE
- ) {
- throw new Error(
- `gen-third-party-notices: installed ${payload.alias} does not match its official ${CODEX_PACKAGE}@${payload.version} payload and ${CODEX_DECLARED_LICENSE} license.`,
- )
- }
- }
- if (installedPayloads === 0) {
- throw new Error(
- 'gen-third-party-notices: no Codex platform payload is installed; install optional dependencies before regenerating.',
- )
- }
- return distribution
+ return codexDistributionFromInstalledPackage(packageJsonPath)
}
/** Normalize a manifest repository/homepage value to a browsable https URL. */
From ffa119e86e1ff17ea0e93a04ed59941c17d58feb Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 03:56:12 +0800
Subject: [PATCH 012/134] docs(subagent): clarify Claude tool opt-in
---
packages/subagent/README.i18n.yaml | 4 ++--
packages/subagent/README.md | 2 +-
packages/subagent/README.zh.md | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/subagent/README.i18n.yaml b/packages/subagent/README.i18n.yaml
index 24fefb0441..ed6dd44e77 100644
--- a/packages/subagent/README.i18n.yaml
+++ b/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: aebf368b984dca3ac2e37d1bcdba26a82ce85196
-README.zh.md: fb9e223f916a8b07d3a4ae254fa61087de081a93
+README.md: e6503c0f9d9861b41a39ad2790b7c8a171fd9bc0
+README.zh.md: 489577b58446d1ccd1ec7393f46d19f94609d73e
diff --git a/packages/subagent/README.md b/packages/subagent/README.md
index aebf368b98..e6503c0f9d 100644
--- a/packages/subagent/README.md
+++ b/packages/subagent/README.md
@@ -18,7 +18,7 @@ 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 Claude Code package is also an optional Profile Bundle. Install it with `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; the package registers only its dormant Host provider, while a copied Agent Preset separately grants the disabled tool template to new Sessions. Removing the package withdraws that provider on the next Profile start. The Codex package remains an explicitly mounted Host plugin and uses a host `codex` from `PATH`.
+The Claude Code package is also an optional Profile Bundle. Install it with `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code`, then restart that Profile; the package registers only its dormant Host provider. To grant the tool, copy a complete Agent Preset, remove `disabled` from the matching tool row, and start a new Session. Removing the package withdraws that provider on the next Profile start. The Codex package remains an explicitly mounted Host plugin and uses a host `codex` from `PATH`.
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).
diff --git a/packages/subagent/README.zh.md b/packages/subagent/README.zh.md
index fb9e223f91..489577b584 100644
--- a/packages/subagent/README.zh.md
+++ b/packages/subagent/README.zh.md
@@ -18,7 +18,7 @@
| [`tool-subagent-control/`](tool-subagent-control/README.md) | 向模型公开子级消息发送和列举操作 | 注册到 `ctx.tools` |
| [`tool-subagent-report/`](tool-subagent-report/README.md) | 提供从子级到父级的报告通道 | 注册到子级作用域 |
-Claude Code 包也是一个可选的 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code` 安装后重启该 Profile;该包只注册休眠的 Host provider,而复制出的 Agent Preset 会单独把默认禁用的工具模板授予新 Session。移除该包后,下一次 Profile 启动会撤回对应 provider。Codex 包仍须作为 Host 插件显式挂载,并使用 `PATH` 中的宿主 `codex`。
+Claude Code 包也是一个可选的 Profile Bundle。使用 `dsh plugin --profile add @deepseek-ai/dsh-subagent-claude-code` 安装后重启该 Profile;该包只注册休眠的 Host provider。要授予工具,请复制一份完整 Agent Preset,删除对应工具行的 `disabled`,再启动新 Session。移除该包后,下一次 Profile 启动会撤回对应 provider。Codex 包仍须作为 Host 插件显式挂载,并使用 `PATH` 中的宿主 `codex`。
参见有关[能力家族](../../.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)的决策。
From 72cb49dbbb90907cc5a8862d65ad6e1dbcfcfd67 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 04:12:41 +0800
Subject: [PATCH 013/134] refactor(subagent): narrow Codex runtime ownership
---
...ubagent-providers-in-shared-host.i18n.yaml | 4 +--
...oduct-subagent-providers-in-shared-host.md | 4 +--
...ct-subagent-providers-in-shared-host.zh.md | 4 +--
packages/subagent/subagent-codex/src/run.ts | 27 +++-------------
.../subagent-codex/tests/real-deepseek.e2e.ts | 14 ++++----
.../subagent-codex/tests/real-product.spec.ts | 11 ++++---
.../tests/subagent-codex.spec.ts | 32 ++++++-------------
7 files changed, 34 insertions(+), 62 deletions(-)
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index 14fc409ebc..482e3aee8e 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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: f1eac30e2984b6b9c1a0e83594a8f48dde690811
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: 14b7afcf414b9c058670d99531385da49d91fa4e
+2026-08-10-product-subagent-providers-in-shared-host.md: 4d3af18f0985ccf3322eac955bf53c108104c5a0
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: 9e29d8c0d05507261bfbca04b6bb5f3fa96b2c12
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index f1eac30e29..4d3af18f09 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -16,11 +16,11 @@ Each product Bundle loads its fixed provider exactly once in the shared Host pla
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 either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
-The Bundles have different executable owners. The Codex package pins the official wrapper and six platform aliases; the provider runs the package-declared wrapper, which selects the private native payload. The Claude Code package pins its Agent SDK and eight platform packages; the provider lets that SDK select the private native executable. Neither provider consults or falls back to a host product command, while native configuration and authentication remain authoritative. Loading either Bundle only registers the provider and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing platform payload, authentication failure, and other product failures remain local to the attempted delegation.
+Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its Agent SDK select the private native executable. Neither provider consults or falls back to a host product command, while native configuration and authentication remain authoritative. Loading either Bundle only registers the provider and creates no product state, probes no version or authentication, and adds no product-specific setting. A missing platform payload, authentication failure, and other product failures remain local to the attempted delegation.
## Verification
-Real composition loads no product Bundle, Codex only, Claude Code only, or both, then crosses that availability with Agent Presets that grant neither tool, either one, or both. It proves the Host registry and model-visible tools reflect those independent decisions, no product process starts during composition, and Preset edits affect only later Sessions. Package Loader and real-product tests separately prove each private runtime, missing-payload failure without host fallback, cancellation, and process-tree quiescence. Keyless ACP snapshots pin the model-visible tool schemas and generic Job controls.
+Real composition loads no product Bundle, Codex only, Claude Code only, or both, then crosses that availability with Agent Presets that grant neither tool, either one, or both. It proves the Host registry and model-visible tools reflect those independent decisions, no product process starts during composition, and Preset edits affect only later Sessions. The linked provider and background decisions own private-runtime, failure, teardown, tool-schema, and Job evidence.
## Alternatives considered
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index 14b7afcf41..9e29d8c0d0 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -16,11 +16,11 @@ Status: implemented
[生产依赖闭包决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只部分取代本说明先前关于默认包含提供方的选择:base 组合包排除两个提供方,每个提供方包都拥有可直接安装的 Bundle patch。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
-两个 Bundle 的可执行文件归属不同。Codex 包锁定官方 wrapper 与六个平台 alias;提供方运行包所声明的 wrapper,再由它选择私有原生载荷。Claude Code 包锁定 Agent SDK 与八个平台包;提供方让 SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令,原生配置与身份验证仍保持权威。加载任一 Bundle 只会完成提供方注册,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
+每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapper,Claude Code 包则让 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令,原生配置与身份验证仍保持权威。加载任一 Bundle 只会完成提供方注册,不会创建产品状态、探测版本或身份验证,也不会新增产品专属设置。平台载荷缺失、身份验证失败和其他产品故障仍局限于发生问题的那次委派。
## 验证
-真实组装会覆盖未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装四种状态,再与不授权工具、只授权其中一个或同时授权两者的 Agent Preset 交叉。测试证明 Host 注册表与模型可见工具会反映这两个独立决策,组装期间不会启动产品进程,而且 Preset 编辑只影响后续 Session。包级 Loader 与真实产品测试分别证明两个私有运行时、载荷缺失时不回退宿主命令、取消和进程树完全停稳。无密钥 ACP(Agent Client Protocol)快照固定模型可见工具 schema 与通用 Job 控制。
+真实组装会覆盖未安装产品 Bundle、仅安装 Codex、仅安装 Claude Code 或两者都安装四种状态,再与不授权工具、只授权其中一个或同时授权两者的 Agent Preset 交叉。测试证明 Host 注册表与模型可见工具会反映这两个独立决策,组装期间不会启动产品进程,而且 Preset 编辑只影响后续 Session。已链接的提供方与后台执行决策分别拥有私有运行时、失败、清理、工具 schema 及 Job 证据。
## 考虑过的替代方案
diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts
index d497a4f16c..47c711aaee 100644
--- a/packages/subagent/subagent-codex/src/run.ts
+++ b/packages/subagent/subagent-codex/src/run.ts
@@ -28,26 +28,9 @@ import { CodexAppServerWire } from './wire.ts'
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
interface CodexPackageManifest {
- readonly bin?: string | Readonly>
-}
-
-/**
- * Resolve the official package's declared `codex` bin relative to its manifest.
- * @param packageJsonPath - absolute path to the official package manifest.
- * @param manifest - parsed manifest carrying the declared bin entry.
- * @returns absolute path to the package-local JavaScript wrapper.
- */
-export function codexPackageBinPath(
- packageJsonPath: string,
- manifest: CodexPackageManifest,
-): string {
- const declared = typeof manifest.bin === 'string'
- ? manifest.bin
- : manifest.bin?.codex
- if (declared === undefined || declared.length === 0) {
- throw new Error('@openai/codex does not declare its codex bin')
+ readonly bin: {
+ readonly codex: string
}
- return resolve(dirname(packageJsonPath), declared)
}
const codexPackageJsonPath = createRequire(import.meta.url).resolve('@openai/codex/package.json')
@@ -56,9 +39,9 @@ const codexPackageManifest = JSON.parse(
) as CodexPackageManifest
/** Absolute package-local JavaScript wrapper selected by the package manifest. */
-export const CODEX_PACKAGE_BIN = codexPackageBinPath(
- codexPackageJsonPath,
- codexPackageManifest,
+const CODEX_PACKAGE_BIN = resolve(
+ dirname(codexPackageJsonPath),
+ codexPackageManifest.bin.codex,
)
/**
diff --git a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
index 2e876134a3..c12e96a306 100644
--- a/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
+++ b/packages/subagent/subagent-codex/tests/real-deepseek.e2e.ts
@@ -7,9 +7,9 @@ import {
rmSync,
writeFileSync,
} from 'node:fs'
+import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
-import { join, resolve } from 'node:path'
-import { fileURLToPath } from 'node:url'
+import { dirname, join, resolve } from 'node:path'
import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -18,18 +18,18 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
-import { CODEX_PACKAGE_BIN } from '../src/run.ts'
import {
startDeepSeekResponsesBridge,
type DeepSeekResponsesBridge,
} from './deepseek-responses-bridge.ts'
const execFileAsync = promisify(execFile)
-const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
+const codexPackageJson = createRequire(import.meta.url).resolve('@openai/codex/package.json')
const codexPackage = JSON.parse(readFileSync(
- join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
+ codexPackageJson,
'utf8',
-)) as { version: string }
+)) as { version: string; bin: { codex: string } }
+const codexEntry = resolve(dirname(codexPackageJson), codexPackage.bin.codex)
const roots: string[] = []
const contexts: Context[] = []
@@ -106,7 +106,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
return handle
})
await ctx.plugin(codex, { env, disposeGraceMs: 2_000 })
- const version = await execFileAsync(process.execPath, [CODEX_PACKAGE_BIN, '--version'], {
+ const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
env: { ...process.env, ...env },
})
expect(codexPackage.version).toBe('0.147.0')
diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts
index 8b1181a743..fb9af718e9 100644
--- a/packages/subagent/subagent-codex/tests/real-product.spec.ts
+++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts
@@ -8,6 +8,7 @@ import {
writeFileSync,
} from 'node:fs'
import { rm } from 'node:fs/promises'
+import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -19,7 +20,6 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
-import { CODEX_PACKAGE_BIN } from '../src/run.ts'
import {
startResponsesFixture,
type ResponsesBehavior,
@@ -29,12 +29,13 @@ import {
const execFileAsync = promisify(execFile)
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
const codexBinDir = join(packageRoot, 'node_modules', '.bin')
-const codexEntry = CODEX_PACKAGE_BIN
-const codexPackageRoot = dirname(dirname(codexEntry))
+const codexPackageJson = createRequire(import.meta.url).resolve('@openai/codex/package.json')
const codexPackage = JSON.parse(readFileSync(
- join(packageRoot, 'node_modules', '@openai', 'codex', 'package.json'),
+ codexPackageJson,
'utf8',
-)) as { version: string }
+)) as { version: string; bin: { codex: string } }
+const codexEntry = resolve(dirname(codexPackageJson), codexPackage.bin.codex)
+const codexPackageRoot = dirname(dirname(codexEntry))
const roots: string[] = []
const fixtures: ResponsesFixture[] = []
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index a60ed2f759..96c30ed52b 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -1,5 +1,5 @@
import { readFileSync } from 'node:fs'
-import { resolve } from 'node:path'
+import { dirname, resolve } from 'node:path'
import { PassThrough } from 'node:stream'
import { fileURLToPath } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
@@ -19,9 +19,7 @@ import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
- CODEX_PACKAGE_BIN,
codexAppServerArgv,
- codexPackageBinPath,
DEFAULT_DISPOSE_GRACE_MS,
disposeCodexChild,
startCodexRun,
@@ -295,7 +293,7 @@ describe('task admission and package contracts', () => {
const codexPackageJson = fileURLToPath(import.meta.resolve('@openai/codex/package.json'))
const codexManifest = JSON.parse(readFileSync(codexPackageJson, 'utf8')) as {
version: string
- bin: Record
+ bin: { codex: string }
optionalDependencies: Record
}
expect(codexManifest.version).toBe(CODEX_VERSION)
@@ -306,7 +304,12 @@ describe('task admission and package contracts', () => {
`npm:@openai/codex@${CODEX_VERSION}-${packageName.slice('@openai/codex-'.length)}`,
]),
))
- expect(CODEX_PACKAGE_BIN).toBe(codexPackageBinPath(codexPackageJson, codexManifest))
+ expect(codexAppServerArgv()).toEqual([
+ process.execPath,
+ resolve(dirname(codexPackageJson), codexManifest.bin.codex),
+ 'app-server',
+ '--stdio',
+ ])
const lockfile = readFileSync(resolve(root, '../../../pnpm-lock.yaml'), 'utf8')
for (const packageName of CODEX_PLATFORM_PACKAGES) {
@@ -329,23 +332,8 @@ describe('task admission and package contracts', () => {
})
it('uses only the official package-declared wrapper for app-server', () => {
- expect(codexPackageBinPath(
- '/package/node_modules/@openai/codex/package.json',
- { bin: 'bin/codex.js' },
- )).toBe('/package/node_modules/@openai/codex/bin/codex.js')
- expect(codexPackageBinPath(
- '/package/node_modules/@openai/codex/package.json',
- { bin: { codex: 'bin/codex.js' } },
- )).toBe('/package/node_modules/@openai/codex/bin/codex.js')
- expect(() => codexPackageBinPath('/package/package.json', {}))
- .toThrow('does not declare its codex bin')
- expect(codexAppServerArgv()).toEqual([
- process.execPath,
- CODEX_PACKAGE_BIN,
- 'app-server',
- '--stdio',
- ])
- expect(codexAppServerArgv()).not.toContain('codex')
+ expect(codexAppServerArgv()[0]).toBe(process.execPath)
+ expect(codexAppServerArgv().slice(2)).toEqual(['app-server', '--stdio'])
})
it('accepts one or more text blocks and rejects empty or non-text tasks', () => {
From bc91897adbdd1932ee510ebd5ede3c49f74d9feb Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 04:24:03 +0800
Subject: [PATCH 014/134] test(cli): load product Bundles through Profile
---
apps/cli/tests/web-agent-presets.e2e.ts | 45 +++++++++++++++++--------
1 file changed, 31 insertions(+), 14 deletions(-)
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 5e9680fd13..0e98af0477 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
-import { boot, healProfilesModuleFallback, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
+import { boot, healProfilesModuleFallback, loadOverlayPatches, loadProfile } from '@deepseek-ai/dsh-app-boot'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -26,8 +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')
+const CODEX_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-codex')
+const CLAUDE_CODE_PACKAGE_DIR = join(REPO_ROOT, 'packages/subagent/subagent-claude-code')
/** The installation anchor whose dependency surface the preset module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
const MINIMAL_PROMPT = 'You are a helpful software engineer assistant.'
@@ -49,11 +49,10 @@ async function bootWeb(
settingsFile: string,
extra: PatchOptions[] = [],
profilePackages: readonly string[] = [],
+ profileBundles?: readonly string[],
): Promise {
const storageRoot = join(dirname(settingsFile), 'storages')
- const patches: PatchOptions[] = [
- ...loadOverlayPatches('dsh-test', BASE_PATCH),
- ...loadOverlayPatches('dsh-test', WEB_PATCH),
+ const overrides: PatchOptions[] = [
// The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
// reads the developer's own document — and since the default preset is a
// setting, a stored `agent-presets.default` would decide this file's
@@ -128,9 +127,22 @@ async function bootWeb(
await mkdir(dirname(link), { recursive: true })
await symlink(packageDir, link, 'junction')
}
+ let bundlePatches: PatchOptions[] = [
+ ...loadOverlayPatches('dsh-test', BASE_PATCH),
+ ...loadOverlayPatches('dsh-test', WEB_PATCH),
+ ]
+ if (profileBundles !== undefined) {
+ await writeFile(join(profileDir, 'package.json'), JSON.stringify({
+ private: true,
+ dependencies: Object.fromEntries(profileBundles.map(name => [name, 'workspace:*'])),
+ dsh: { profile: { bundles: profileBundles } },
+ }, null, 2) + '\n')
+ const profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
+ bundlePatches = profile.layers.flatMap(layer => layer.patches)
+ }
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
- return await boot('dsh-test', rootConfig, patches, (bootCtx) => {
+ return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], (bootCtx) => {
provideCmdline(bootCtx, { args: [], exit: () => {} })
})
}
@@ -467,14 +479,15 @@ describe('product Bundle and user-preset intersection', () => {
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'agent.cordis.yml'), composition)
}
- const patchPath = (product: Product): string => (
- product === 'codex' ? CODEX_PATCH : CLAUDE_CODE_PATCH
+ const packageDir = (product: Product): string => (
+ product === 'codex' ? CODEX_PACKAGE_DIR : CLAUDE_CODE_PACKAGE_DIR
+ )
+ const packageName = (product: Product): string => (
+ product === 'codex'
+ ? '@deepseek-ai/dsh-subagent-codex'
+ : '@deepseek-ai/dsh-subagent-claude-code'
)
- const productPatches = installed.flatMap(product => (
- loadOverlayPatches('dsh-test', patchPath(product))
- ))
return await bootWeb(settingsFile, [
- ...productPatches,
{
id: 'agent-presets',
config: {
@@ -486,7 +499,11 @@ describe('product Bundle and user-preset intersection', () => {
includeUserRoot: false,
},
},
- ], installed.map(product => dirname(patchPath(product))))
+ ], installed.map(packageDir), [
+ '@deepseek-ai/dsh-base',
+ '@deepseek-ai/dsh-web-app',
+ ...installed.map(packageName),
+ ])
}
it('composes the intersection of installed Bundles and enabled preset rows', async () => {
From d1a767c66b34b7de8c97f56497d8b6594e87956f Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 04:32:30 +0800
Subject: [PATCH 015/134] test(infra): isolate Codex alias fixture
---
scripts/gen-third-party-notices.spec.ts | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts
index bd78e6f0fc..363328ca2f 100644
--- a/scripts/gen-third-party-notices.spec.ts
+++ b/scripts/gen-third-party-notices.spec.ts
@@ -387,12 +387,7 @@ describe('official Codex platform payloads', () => {
fixtureRoot,
'node_modules/@openai/codex/package.json',
)
- const payloadPath = join(
- fixtureRoot,
- 'node_modules/@openai/codex-darwin-arm64/package.json',
- )
mkdirSync(resolve(wrapperPath, '..'), { recursive: true })
- mkdirSync(resolve(payloadPath, '..'), { recursive: true })
writeFileSync(wrapperPath, JSON.stringify({
name: CODEX_PACKAGE,
version: '9.8.7',
@@ -401,11 +396,18 @@ describe('official Codex platform payloads', () => {
'@openai/codex-linux-x64': 'npm:@openai/codex@9.8.7-linux-x64',
},
}))
- writeFileSync(payloadPath, JSON.stringify({
- name: CODEX_PACKAGE,
- version: '9.8.7-darwin-arm64',
- license: 'Apache-2.0',
- }))
+ for (const platform of ['darwin-arm64', 'linux-x64']) {
+ const payloadPath = join(
+ fixtureRoot,
+ `node_modules/@openai/codex-${platform}/package.json`,
+ )
+ mkdirSync(resolve(payloadPath, '..'), { recursive: true })
+ writeFileSync(payloadPath, JSON.stringify({
+ name: CODEX_PACKAGE,
+ version: `9.8.7-${platform}`,
+ license: 'Apache-2.0',
+ }))
+ }
expect(codexDistributionFromInstalledPackage(wrapperPath)).toEqual({
wrapperVersion: '9.8.7',
From 4775095aa2b2488a5d179dd56d1a9eb133983aec Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 04:41:50 +0800
Subject: [PATCH 016/134] fix(infra): remove stale Codex dependency hints
---
knip.json | 3 ---
packages/subagent/subagent-codex/package.json | 1 -
2 files changed, 4 deletions(-)
diff --git a/knip.json b/knip.json
index 3017292382..b4c9808971 100644
--- a/knip.json
+++ b/knip.json
@@ -636,9 +636,6 @@
"project": [
"src/**/*.ts",
"tests/**/*.ts"
- ],
- "ignoreDependencies": [
- "@openai/codex"
]
},
"packages/subagent/subagent-claude-code": {
diff --git a/packages/subagent/subagent-codex/package.json b/packages/subagent/subagent-codex/package.json
index 1d948eb631..8144ff9c4c 100644
--- a/packages/subagent/subagent-codex/package.json
+++ b/packages/subagent/subagent-codex/package.json
@@ -57,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:^",
From cb229c896450b160e95cf6a5cfee5f31cb996e62 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 05:26:12 +0800
Subject: [PATCH 017/134] refactor(infra): keep Codex notices direct
---
THIRD_PARTY_NOTICES.md | 16 +--
scripts/gen-third-party-notices.spec.ts | 91 ---------------
scripts/gen-third-party-notices.ts | 140 +-----------------------
3 files changed, 2 insertions(+), 245 deletions(-)
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index b03dad17cd..a62d3ed8ac 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -5,7 +5,7 @@
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
-This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code and Codex platform payload closures. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
+This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [`pnpm-lock.yaml`](pnpm-lock.yaml) — inspect it with `pnpm licenses list`. The Python closure is recorded separately in [`python/sdk/uv.lock`](python/sdk/uv.lock).
@@ -114,20 +114,6 @@ The installed SDK 0.3.220 declares the following optional platform packages. Eac
| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md |
-## Official Codex platform payloads
-
-The installed `@openai/codex` wrapper 0.147.0 declares the following optional-dependency aliases. Every alias resolves to an official platform-specific `@openai/codex` version that carries the native Codex CLI and its bundled resources; the declared license is verified against the payload installed for the current host.
-
-| Optional dependency alias | Published package | Version | Declared license |
-| --- | --- | --- | --- |
-| `@openai/codex-darwin-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-darwin-arm64) | 0.147.0-darwin-arm64 | Apache-2.0 |
-| `@openai/codex-darwin-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-darwin-x64) | 0.147.0-darwin-x64 | Apache-2.0 |
-| `@openai/codex-linux-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-linux-arm64) | 0.147.0-linux-arm64 | Apache-2.0 |
-| `@openai/codex-linux-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-linux-x64) | 0.147.0-linux-x64 | Apache-2.0 |
-| `@openai/codex-win32-arm64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-win32-arm64) | 0.147.0-win32-arm64 | Apache-2.0 |
-| `@openai/codex-win32-x64` | [`@openai/codex`](https://www.npmjs.com/package/@openai/codex/v/0.147.0-win32-x64) | 0.147.0-win32-x64 | Apache-2.0 |
-
-
## Development-only npm dependencies
External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts
index 363328ca2f..479a2f13b1 100644
--- a/scripts/gen-third-party-notices.spec.ts
+++ b/scripts/gen-third-party-notices.spec.ts
@@ -4,10 +4,7 @@ import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import {
CLAUDE_AGENT_SDK_PACKAGE,
- CODEX_PACKAGE,
claudeDistributionFromManifest,
- codexDistributionFromManifest,
- codexDistributionFromInstalledPackage,
collectPythonDependencies,
isOwnerAuthorizedRuntime,
isPermissive,
@@ -334,94 +331,6 @@ describe('official Claude distribution authorization', () => {
})
})
-describe('official Codex platform payloads', () => {
- it('derives versioned packages from the wrapper aliases', () => {
- expect(codexDistributionFromManifest({
- name: CODEX_PACKAGE,
- version: '9.8.7',
- optionalDependencies: {
- '@openai/codex-linux-x64': 'npm:@openai/codex@9.8.7-linux-x64',
- '@openai/codex-darwin-arm64': 'npm:@openai/codex@9.8.7-darwin-arm64',
- },
- })).toEqual({
- wrapperVersion: '9.8.7',
- payloads: [
- { alias: '@openai/codex-darwin-arm64', version: '9.8.7-darwin-arm64' },
- { alias: '@openai/codex-linux-x64', version: '9.8.7-linux-x64' },
- ],
- })
- })
-
- it('rejects a wrong identity, missing payloads, and non-official aliases', () => {
- expect(() => codexDistributionFromManifest({
- name: '@openai/unrelated',
- version: '1.0.0',
- optionalDependencies: {
- '@openai/codex-linux-x64': 'npm:@openai/codex@1.0.0-linux-x64',
- },
- })).toThrow(`expected ${CODEX_PACKAGE} manifest`)
- expect(() => codexDistributionFromManifest({
- name: CODEX_PACKAGE,
- version: '1.0.0',
- })).toThrow('declares no optional platform payloads')
- expect(() => codexDistributionFromManifest({
- name: CODEX_PACKAGE,
- version: '1.0.0',
- optionalDependencies: {
- '@openai/unrelated': 'npm:@openai/codex@1.0.0-linux-x64',
- },
- })).toThrow('outside its platform alias namespace')
- expect(() => codexDistributionFromManifest({
- name: CODEX_PACKAGE,
- version: '1.0.0',
- optionalDependencies: {
- '@openai/codex-linux-x64': '1.0.0',
- },
- })).toThrow('does not alias an official versioned payload')
- })
-
- it('resolves installed payload aliases from the wrapper package', () => {
- const fixtureRoot = mkdtempSync(join(tmpdir(), 'dsh-notices-codex-wrapper-'))
- try {
- const wrapperPath = join(
- fixtureRoot,
- 'node_modules/@openai/codex/package.json',
- )
- mkdirSync(resolve(wrapperPath, '..'), { recursive: true })
- writeFileSync(wrapperPath, JSON.stringify({
- name: CODEX_PACKAGE,
- version: '9.8.7',
- optionalDependencies: {
- '@openai/codex-darwin-arm64': 'npm:@openai/codex@9.8.7-darwin-arm64',
- '@openai/codex-linux-x64': 'npm:@openai/codex@9.8.7-linux-x64',
- },
- }))
- for (const platform of ['darwin-arm64', 'linux-x64']) {
- const payloadPath = join(
- fixtureRoot,
- `node_modules/@openai/codex-${platform}/package.json`,
- )
- mkdirSync(resolve(payloadPath, '..'), { recursive: true })
- writeFileSync(payloadPath, JSON.stringify({
- name: CODEX_PACKAGE,
- version: `9.8.7-${platform}`,
- license: 'Apache-2.0',
- }))
- }
-
- expect(codexDistributionFromInstalledPackage(wrapperPath)).toEqual({
- wrapperVersion: '9.8.7',
- payloads: [
- { alias: '@openai/codex-darwin-arm64', version: '9.8.7-darwin-arm64' },
- { alias: '@openai/codex-linux-x64', version: '9.8.7-linux-x64' },
- ],
- })
- } finally {
- rmSync(fixtureRoot, { recursive: true, force: true })
- }
- })
-})
-
describe('manifestPatterns', () => {
it('derives globs from the declared members, so a new member area is read', () => {
expect(manifestPatterns(['packages/*/*', 'tools/*', 'native/landlock-run', 'native/landlock-run/packages/*'])).toEqual([
diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts
index 314c9b1b28..3362251f1e 100644
--- a/scripts/gen-third-party-notices.ts
+++ b/scripts/gen-third-party-notices.ts
@@ -9,7 +9,6 @@
*/
import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
@@ -51,9 +50,6 @@ const FIRST_PARTY = new Set([
export const CLAUDE_AGENT_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk'
const CLAUDE_PLATFORM_PACKAGE_PREFIX = `${CLAUDE_AGENT_SDK_PACKAGE}-`
const CLAUDE_PLATFORM_DECLARED_LICENSE = 'SEE LICENSE IN LICENSE.md'
-export const CODEX_PACKAGE = '@openai/codex'
-const CODEX_PLATFORM_ALIAS_PREFIX = `${CODEX_PACKAGE}-`
-const CODEX_DECLARED_LICENSE = 'Apache-2.0'
/**
* Whether a non-permissive runtime declaration has an identity-scoped owner
@@ -198,18 +194,6 @@ export interface ClaudeDistribution {
readonly payloads: ClaudePlatformPayload[]
}
-/** One optional-dependency alias for an official Codex platform payload. */
-export interface CodexPlatformPayload {
- readonly alias: string
- readonly version: string
-}
-
-/** Current Codex wrapper and platform payload facts from the official manifest. */
-export interface CodexDistribution {
- readonly wrapperVersion: string
- readonly payloads: CodexPlatformPayload[]
-}
-
function requiredManifestString(
value: string | undefined,
field: string,
@@ -259,91 +243,6 @@ export function claudeDistributionFromManifest(
return { sdkVersion, claudeCodeVersion, payloads }
}
-/** Derive official Codex platform aliases and published package versions. */
-export function codexDistributionFromManifest(
- manifest: VirtualManifest,
-): CodexDistribution {
- if (manifest.name !== CODEX_PACKAGE) {
- throw new Error(
- `gen-third-party-notices: expected ${CODEX_PACKAGE} manifest, got ${JSON.stringify(manifest.name)}.`,
- )
- }
- const wrapperVersion = manifest.version
- if (wrapperVersion === undefined || wrapperVersion.length === 0) {
- throw new Error(`gen-third-party-notices: ${CODEX_PACKAGE} has no version.`)
- }
- const entries = Object.entries(manifest.optionalDependencies ?? {})
- if (entries.length === 0) {
- throw new Error(`gen-third-party-notices: ${CODEX_PACKAGE} declares no optional platform payloads.`)
- }
- const payloads = entries.map(([alias, spec]) => {
- if (!alias.startsWith(CODEX_PLATFORM_ALIAS_PREFIX)) {
- throw new Error(
- `gen-third-party-notices: ${CODEX_PACKAGE} optional dependency ${alias} is outside its platform alias namespace.`,
- )
- }
- const prefix = `npm:${CODEX_PACKAGE}@`
- if (!spec.startsWith(prefix) || spec.length === prefix.length) {
- throw new Error(
- `gen-third-party-notices: ${CODEX_PACKAGE} optional dependency ${alias} does not alias an official versioned payload.`,
- )
- }
- return { alias, version: spec.slice(prefix.length) }
- }).sort((left, right) => left.alias.localeCompare(right.alias))
- return { wrapperVersion, payloads }
-}
-
-function requireManifest(
- requireFrom: NodeJS.Require,
- name: string,
-): VirtualManifest | undefined {
- let packageJsonPath: string
- try {
- packageJsonPath = requireFrom.resolve(`${name}/package.json`)
- } catch (error: unknown) {
- if (error instanceof Error && 'code' in error && error.code === 'MODULE_NOT_FOUND') {
- return undefined
- }
- throw error
- }
- return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as VirtualManifest
-}
-
-/**
- * Derive and verify the Codex distribution from the wrapper package's own
- * Node resolution context.
- * @param packageJsonPath - absolute manifest path for the installed wrapper.
- * @returns the wrapper version and all declared platform aliases.
- */
-export function codexDistributionFromInstalledPackage(
- packageJsonPath: string,
-): CodexDistribution {
- const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as VirtualManifest
- const distribution = codexDistributionFromManifest(manifest)
- const requireFromWrapper = createRequire(packageJsonPath)
- let installedPayloads = 0
- for (const payload of distribution.payloads) {
- const installed = requireManifest(requireFromWrapper, payload.alias)
- if (installed === undefined) continue
- installedPayloads += 1
- if (
- installed.name !== CODEX_PACKAGE
- || installed.version !== payload.version
- || installed.license !== CODEX_DECLARED_LICENSE
- ) {
- throw new Error(
- `gen-third-party-notices: installed ${payload.alias} does not match its official ${CODEX_PACKAGE}@${payload.version} payload and ${CODEX_DECLARED_LICENSE} license.`,
- )
- }
- }
- if (installedPayloads === 0) {
- throw new Error(
- 'gen-third-party-notices: no Codex platform payload is installed; install optional dependencies before regenerating.',
- )
- }
- return distribution
-}
-
/**
* Resolve one package's manifest inside a pnpm virtual store. The prefix scan
* matches ordinary `@scope+name@version` directory names; pnpm 11 truncates
@@ -434,20 +333,6 @@ function collectClaudeDistribution(): ClaudeDistribution {
return distribution
}
-function collectCodexDistribution(): CodexDistribution {
- const requireFromProvider = createRequire(resolve(
- root,
- 'packages/subagent/subagent-codex/package.json',
- ))
- let packageJsonPath: string
- try {
- packageJsonPath = requireFromProvider.resolve(`${CODEX_PACKAGE}/package.json`)
- } catch {
- throw new Error(`gen-third-party-notices: cannot resolve ${CODEX_PACKAGE}; run \`pnpm install\`.`)
- }
- return codexDistributionFromInstalledPackage(packageJsonPath)
-}
-
/** Normalize a manifest repository/homepage value to a browsable https URL. */
function normalizeRepo(raw: string | undefined): string | undefined {
if (raw === undefined || raw === '') return undefined
@@ -771,24 +656,6 @@ ${rows.join('\n')}
`
}
-function renderCodexDistribution(
- distribution: CodexDistribution | undefined,
-): string {
- if (distribution === undefined) return ''
- const rows = distribution.payloads.map(payload => (
- `| \`${payload.alias}\` | [\`${CODEX_PACKAGE}\`](https://www.npmjs.com/package/${CODEX_PACKAGE}/v/${payload.version}) | ${payload.version} | ${CODEX_DECLARED_LICENSE} |`
- ))
- return `
-## Official Codex platform payloads
-
-The installed \`${CODEX_PACKAGE}\` wrapper ${distribution.wrapperVersion} declares the following optional-dependency aliases. Every alias resolves to an official platform-specific \`${CODEX_PACKAGE}\` version that carries the native Codex CLI and its bundled resources; the declared license is verified against the payload installed for the current host.
-
-| Optional dependency alias | Published package | Version | Declared license |
-| --- | --- | --- | --- |
-${rows.join('\n')}
-`
-}
-
/**
* Render the complete notices document.
* @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
@@ -806,10 +673,6 @@ export function render(): string {
)
? collectClaudeDistribution()
: undefined
- const codexDistribution = runtimeDeps.some(dep => dep.name === CODEX_PACKAGE)
- ? collectCodexDistribution()
- : undefined
-
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
// A copyleft license reaching a shipped surface is a distribution decision,
// not a rendering detail; the notices cannot quietly absorb it.
@@ -829,7 +692,7 @@ export function render(): string {
DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
-This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code and Codex platform payload closures. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
+This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
@@ -851,7 +714,6 @@ pnpm applies local patches to the following packages at install time, so shipped
${patchedLines.join('\n')}
${renderClaudeDistribution(claudeDistribution)}
-${renderCodexDistribution(codexDistribution)}
## Development-only npm dependencies
From ab696d84c20031b08dc7c3d957c9bf0e68654e48 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 05:36:23 +0800
Subject: [PATCH 018/134] docs(subagent): show disabled Claude tool row
---
packages/subagent/subagent-claude-code/README.i18n.yaml | 4 ++--
packages/subagent/subagent-claude-code/README.md | 1 +
packages/subagent/subagent-claude-code/README.zh.md | 1 +
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index eaa28043f7..264ad5f71a 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/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: 6083d70c3d70e09f55d228faa14156a2212b4aa9
-README.zh.md: f0db9e90eb0588bc8bbc24a8f404815cc7ad2beb
+README.md: 1d6910856c098f5efa5b9aa9f6d6ef6e14fbd2b4
+README.zh.md: 8d451bde1d291aedea471178c4a5b274073bc3d8
diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md
index 6083d70c3d..1d6910856c 100644
--- a/packages/subagent/subagent-claude-code/README.md
+++ b/packages/subagent/subagent-claude-code/README.md
@@ -53,6 +53,7 @@ Installation controls Host availability, not model permission. Full Agent Preset
# A copied Agent Preset; remove `disabled` to grant this tool.
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index f0db9e90eb..8d451bde1d 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/packages/subagent/subagent-claude-code/README.zh.md
@@ -53,6 +53,7 @@ dsh --profile
# A copied Agent Preset; remove `disabled` to grant this tool.
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
+ disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
From ac9351594393fb8e3c3cde9ee2e6c58f3f01633f Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 05:49:35 +0800
Subject: [PATCH 019/134] fix(subagent): preserve Codex payload diagnostics
---
packages/subagent/subagent-codex/src/run.ts | 37 ++++++++++++++---
.../tests/subagent-codex.spec.ts | 41 ++++++++++++++++++-
2 files changed, 70 insertions(+), 8 deletions(-)
diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts
index 47c711aaee..22fe3a1a34 100644
--- a/packages/subagent/subagent-codex/src/run.ts
+++ b/packages/subagent/subagent-codex/src/run.ts
@@ -26,6 +26,8 @@ import { CodexAppServerWire } from './wire.ts'
/** Default POSIX grace between subprocess termination tiers. */
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
+/** Bounded stderr tail retained only to recognize the wrapper's payload error. */
+const CODEX_STDERR_TAIL_BYTES = 16 * 1024
interface CodexPackageManifest {
readonly bin: {
@@ -44,6 +46,21 @@ const CODEX_PACKAGE_BIN = resolve(
codexPackageManifest.bin.codex,
)
+function missingPayloadDiagnostic(child: SubprocessHandle): string | undefined {
+ const stderr = child.collected.stderr?.readFrom(0).text
+ if (stderr === undefined) return undefined
+ return /Missing optional dependency[^\r\n]*/.exec(stderr)?.[0]?.trim()
+}
+
+function withMissingPayloadDiagnostic(
+ error: Error,
+ child: SubprocessHandle,
+): Error {
+ const diagnostic = missingPayloadDiagnostic(child)
+ if (diagnostic === undefined || error.message.includes(diagnostic)) return error
+ return new Error(`${error.message}: ${diagnostic}`, { cause: error })
+}
+
/**
* Fixed package-local app-server command, independent of the host `PATH`.
* @returns Node, the official wrapper, and the fixed app-server arguments.
@@ -136,7 +153,11 @@ export async function startCodexRun(
const child = spec.spawn({
argv: codexAppServerArgv(),
cwd: spec.cwd,
- stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
+ stdio: {
+ stdin: 'pipe',
+ stdout: 'pipe',
+ stderr: { maxBytes: CODEX_STDERR_TAIL_BYTES },
+ },
graceMs: spec.disposeGraceMs,
env: spec.env,
})
@@ -148,9 +169,12 @@ export async function startCodexRun(
const disposeProcess = (): Promise => disposeCodexChild(wire, child)
const processFailure: Promise = child.done.then(
- outcome => Promise.reject(new Error(
- 'subagent-codex: app-server exited before the run settled '
- + `(code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`,
+ outcome => Promise.reject(withMissingPayloadDiagnostic(
+ new Error(
+ 'subagent-codex: app-server exited before the run settled '
+ + `(code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`,
+ ),
+ child,
)),
(error: unknown) => Promise.reject(thrown(error)),
)
@@ -173,18 +197,19 @@ export async function startCodexRun(
await Promise.race([wire.startThread(spec.cwd, request.signal), processFailure])
} catch (error: unknown) {
request.signal.removeEventListener('abort', onAbort)
+ const startupError = withMissingPayloadDiagnostic(thrown(error), child)
try {
await disposeProcess()
} catch (disposeError: unknown) {
throw new AggregateError(
- [thrown(error), thrown(disposeError)],
+ [startupError, thrown(disposeError)],
'subagent-codex: startup failed and app-server cleanup also failed',
)
}
if (runAbort.signal.aborted) {
throw new Error('subagent-codex: request was aborted before run publication')
}
- throw thrown(error)
+ throw startupError
}
const collectOutput = (): ContentBlock[] => wire.collectOutput()
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 96c30ed52b..476dde2771 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -108,6 +108,7 @@ interface FakeChildOptions {
readonly pid?: number
readonly exitOnTerminate?: boolean
readonly doneError?: Error
+ readonly stderr?: string
}
interface FakeChild {
@@ -125,6 +126,7 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
const fromChild = new PassThrough()
const toChild = new PassThrough()
const peer = new ProtocolPeer(toChild, fromChild)
+ const stderrText = options.stderr
let exited = false
let resolveDone!: (outcome: SubprocessOutcome) => void
let rejectDone!: (error: Error) => void
@@ -174,7 +176,17 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
stdin: toChild,
stdout: fromChild,
stderr: undefined,
- collected: {},
+ collected: stderrText === undefined
+ ? {}
+ : {
+ stderr: {
+ readFrom: () => ({
+ text: stderrText,
+ nextOffset: Buffer.byteLength(stderrText),
+ lossy: false,
+ }),
+ },
+ },
done,
terminate,
waitForExit,
@@ -927,7 +939,11 @@ describe('run lifecycle and quiescence', () => {
expect(spawn).toHaveBeenCalledWith({
argv: codexAppServerArgv(),
cwd: process.cwd(),
- stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
+ stdio: {
+ stdin: 'pipe',
+ stdout: 'pipe',
+ stderr: { maxBytes: 16 * 1024 },
+ },
graceMs: DEFAULT_DISPOSE_GRACE_MS,
env: { OPENAI_API_KEY: 'fake' },
})
@@ -1051,6 +1067,27 @@ describe('run lifecycle and quiescence', () => {
expect(child.terminate).toHaveBeenCalledTimes(1)
})
+ it('surfaces only the wrapper missing-payload diagnostic during startup', async () => {
+ const child = fakeChild({
+ stderr: [
+ 'credential-like unrelated stderr',
+ 'Error: Missing optional dependency @openai/codex-linux-x64.',
+ ].join('\n'),
+ })
+ const starting = startCodexRun(request(), runSpec(child))
+ child.settle({ exitCode: 1, signal: null })
+
+ const error: unknown = await starting.then(
+ () => undefined,
+ (failure: unknown) => failure,
+ )
+ expect(error).toBeInstanceOf(Error)
+ if (!(error instanceof Error)) throw new Error('expected startup failure')
+ expect(error.message).toContain('Missing optional dependency @openai/codex-linux-x64')
+ expect(error.message).not.toContain('credential-like unrelated stderr')
+ expect(child.terminate).toHaveBeenCalledTimes(1)
+ })
+
it('keeps overlapping runs isolated', async () => {
const first = fakeChild()
const second = fakeChild()
From dbea39a125a787163b7725a1eaf01027abf2ade4 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 06:03:47 +0800
Subject: [PATCH 020/134] fix(subagent): sample Codex diagnostics after cleanup
---
packages/subagent/subagent-codex/src/run.ts | 6 +++---
.../tests/subagent-codex.spec.ts | 21 +++++++++++++++++--
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts
index 22fe3a1a34..0f3503859a 100644
--- a/packages/subagent/subagent-codex/src/run.ts
+++ b/packages/subagent/subagent-codex/src/run.ts
@@ -197,19 +197,19 @@ export async function startCodexRun(
await Promise.race([wire.startThread(spec.cwd, request.signal), processFailure])
} catch (error: unknown) {
request.signal.removeEventListener('abort', onAbort)
- const startupError = withMissingPayloadDiagnostic(thrown(error), child)
+ const startupCause = thrown(error)
try {
await disposeProcess()
} catch (disposeError: unknown) {
throw new AggregateError(
- [startupError, thrown(disposeError)],
+ [withMissingPayloadDiagnostic(startupCause, child), thrown(disposeError)],
'subagent-codex: startup failed and app-server cleanup also failed',
)
}
if (runAbort.signal.aborted) {
throw new Error('subagent-codex: request was aborted before run publication')
}
- throw startupError
+ throw withMissingPayloadDiagnostic(startupCause, child)
}
const collectOutput = (): ContentBlock[] => wire.collectOutput()
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index 476dde2771..b5a1baa1c8 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -108,6 +108,7 @@ interface FakeChildOptions {
readonly pid?: number
readonly exitOnTerminate?: boolean
readonly doneError?: Error
+ readonly collectStderr?: boolean
readonly stderr?: string
}
@@ -118,6 +119,7 @@ interface FakeChild {
readonly toChild: PassThrough
readonly settle: (outcome?: SubprocessOutcome) => void
readonly fail: (error: Error) => void
+ readonly setStderr: (text: string) => void
readonly terminate: () => void
readonly waitForExit: (signal?: AbortSignal) => Promise
}
@@ -126,7 +128,7 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
const fromChild = new PassThrough()
const toChild = new PassThrough()
const peer = new ProtocolPeer(toChild, fromChild)
- const stderrText = options.stderr
+ let stderrText = options.stderr ?? ''
let exited = false
let resolveDone!: (outcome: SubprocessOutcome) => void
let rejectDone!: (error: Error) => void
@@ -176,7 +178,7 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
stdin: toChild,
stdout: fromChild,
stderr: undefined,
- collected: stderrText === undefined
+ collected: options.stderr === undefined && options.collectStderr !== true
? {}
: {
stderr: {
@@ -198,6 +200,7 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
toChild,
settle,
fail,
+ setStderr: (text: string): void => { stderrText = text },
terminate,
waitForExit,
}
@@ -1088,6 +1091,20 @@ describe('run lifecycle and quiescence', () => {
expect(child.terminate).toHaveBeenCalledTimes(1)
})
+ it('waits for process settlement before sampling the missing-payload diagnostic', async () => {
+ const child = fakeChild({ collectStderr: true, exitOnTerminate: false })
+ const starting = startCodexRun(request(), runSpec(child))
+ child.fromChild.end()
+ await vi.waitFor(() => { expect(child.terminate).toHaveBeenCalledTimes(1) })
+
+ child.setStderr('Error: Missing optional dependency @openai/codex-linux-x64.')
+ child.settle({ exitCode: 1, signal: null })
+
+ await expect(starting).rejects.toThrow(
+ 'Missing optional dependency @openai/codex-linux-x64',
+ )
+ })
+
it('keeps overlapping runs isolated', async () => {
const first = fakeChild()
const second = fakeChild()
From 02e4100f2a09253dd967ee94c2ff572ffaf0669b Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 06:08:31 +0800
Subject: [PATCH 021/134] fix(subagent): normalize Codex payload diagnostics
---
packages/subagent/subagent-codex/src/run.ts | 6 +++++-
.../subagent/subagent-codex/tests/subagent-codex.spec.ts | 5 ++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/packages/subagent/subagent-codex/src/run.ts b/packages/subagent/subagent-codex/src/run.ts
index 0f3503859a..3e84b66c8c 100644
--- a/packages/subagent/subagent-codex/src/run.ts
+++ b/packages/subagent/subagent-codex/src/run.ts
@@ -49,7 +49,11 @@ const CODEX_PACKAGE_BIN = resolve(
function missingPayloadDiagnostic(child: SubprocessHandle): string | undefined {
const stderr = child.collected.stderr?.readFrom(0).text
if (stderr === undefined) return undefined
- return /Missing optional dependency[^\r\n]*/.exec(stderr)?.[0]?.trim()
+ const platformPackage = /Missing optional dependency (@openai\/codex-[a-z0-9-]+)/
+ .exec(stderr)?.[1]
+ return platformPackage === undefined
+ ? undefined
+ : `Missing optional dependency ${platformPackage}`
}
function withMissingPayloadDiagnostic(
diff --git a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
index b5a1baa1c8..9d295864e2 100644
--- a/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
+++ b/packages/subagent/subagent-codex/tests/subagent-codex.spec.ts
@@ -1074,7 +1074,8 @@ describe('run lifecycle and quiescence', () => {
const child = fakeChild({
stderr: [
'credential-like unrelated stderr',
- 'Error: Missing optional dependency @openai/codex-linux-x64.',
+ 'Error: Missing optional dependency @openai/codex-linux-x64. '
+ + 'Reinstall Codex: pnpm add -g @openai/codex@latest',
].join('\n'),
})
const starting = startCodexRun(request(), runSpec(child))
@@ -1088,6 +1089,8 @@ describe('run lifecycle and quiescence', () => {
if (!(error instanceof Error)) throw new Error('expected startup failure')
expect(error.message).toContain('Missing optional dependency @openai/codex-linux-x64')
expect(error.message).not.toContain('credential-like unrelated stderr')
+ expect(error.message).not.toContain('Reinstall Codex')
+ expect(error.message).not.toContain('pnpm add -g')
expect(child.terminate).toHaveBeenCalledTimes(1)
})
From 1446e36a036cf4d8a551878fc3e2961ec6701125 Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 06:45:30 +0800
Subject: [PATCH 022/134] test(subagent): pin Codex payload diagnostic
---
packages/subagent/subagent-codex/tests/real-product.spec.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/subagent/subagent-codex/tests/real-product.spec.ts b/packages/subagent/subagent-codex/tests/real-product.spec.ts
index fb9af718e9..00f824bcf9 100644
--- a/packages/subagent/subagent-codex/tests/real-product.spec.ts
+++ b/packages/subagent/subagent-codex/tests/real-product.spec.ts
@@ -202,7 +202,7 @@ describe('real @openai/codex 0.147.0 product', () => {
? { SystemRoot: process.env.SystemRoot }
: {},
},
- })).rejects.toThrow('Missing optional dependency')
+ })).rejects.toThrow(/Missing optional dependency @openai\/codex-[a-z0-9-]+/)
}, 30_000)
it('cancels a real app-server command approval without executing the command', async () => {
From 4d03472cd098dc48a630e526ca620f4f37f18a0e Mon Sep 17 00:00:00 2001
From: pku-xht
Date: Sat, 15 Aug 2026 16:36:32 +0800
Subject: [PATCH 023/134] feat(subagent): add Claude Code non-interactive
permission modes
---
...ubagent-providers-in-shared-host.i18n.yaml | 4 +-
...oduct-subagent-providers-in-shared-host.md | 2 +-
...ct-subagent-providers-in-shared-host.zh.md | 2 +-
...nt-empty-terminal-message-output.i18n.yaml | 4 +-
...-subagent-empty-terminal-message-output.md | 2 +-
...bagent-empty-terminal-message-output.zh.md | 2 +-
...6-06-21-subagent-capability-seam.i18n.yaml | 4 +-
.../2026-06-21-subagent-capability-seam.md | 4 +-
.../2026-06-21-subagent-capability-seam.zh.md | 4 +-
...-07-08-background-subagent-tasks.i18n.yaml | 4 +-
.../2026-07-08-background-subagent-tasks.md | 6 +-
...2026-07-08-background-subagent-tasks.zh.md | 6 +-
...code-and-codex-subagent-backends.i18n.yaml | 4 +-
...claude-code-and-codex-subagent-backends.md | 12 +-
...ude-code-and-codex-subagent-backends.zh.md | 12 +-
...bagent-one-shot-background-tasks.i18n.yaml | 4 +-
...duct-subagent-one-shot-background-tasks.md | 12 +-
...t-subagent-one-shot-background-tasks.zh.md | 12 +-
...agent-noninteractive-permissions.i18n.yaml | 6 +
...uct-subagent-noninteractive-permissions.md | 72 +++++++
...-subagent-noninteractive-permissions.zh.md | 72 +++++++
docs/config-catalog.i18n.yaml | 4 +-
docs/config-catalog.md | 14 +-
docs/config-catalog.zh.md | 14 +-
docs/subsystems/subagent.i18n.yaml | 4 +-
docs/subsystems/subagent.md | 9 +-
docs/subsystems/subagent.zh.md | 9 +-
.../product-subagent-both.cordis.snapshot.yml | 2 +
.../product-subagent-both.cordis.yml | 2 +
...gent-result-diagnostic.cordis.snapshot.yml | 29 +++
.../subagent-result-diagnostic.cordis.yml | 17 ++
examples/acp-agent/tests/acp.snapshot.ts | 16 +-
.../fixtures/subagent-result-diagnostic.ts | 50 +++++
.../subagent/subagent-claude-code/cordis.yml | 2 +
.../input.json | 7 +
.../replay.override.json | 42 ++++
.../session.jsonl | 51 +++++
.../stdout.expected.jsonl | 4 +
knip.json | 1 +
.../extensions/tool-cordis/src/api-catalog.ts | 2 +-
.../subagent-claude-code/README.i18n.yaml | 4 +-
.../subagent/subagent-claude-code/README.md | 24 ++-
.../subagent-claude-code/README.zh.md | 24 ++-
.../subagent-claude-code/src/index.ts | 18 +-
.../subagent/subagent-claude-code/src/run.ts | 103 ++++++++-
.../tests/messages-fixture.ts | 74 +++++++
.../tests/real-product.spec.ts | 73 ++++++-
.../tests/subagent-claude-code.spec.ts | 198 ++++++++++++++++--
packages/subagent/subagent/README.i18n.yaml | 4 +-
packages/subagent/subagent/README.md | 2 +-
packages/subagent/subagent/README.zh.md | 2 +-
.../subagent/subagent/src/out-of-process.ts | 42 +++-
.../subagent/subagent/src/run-settlement.ts | 12 +-
packages/subagent/subagent/src/types.ts | 7 +
.../subagent/tests/run-settlement.spec.ts | 59 +++++-
.../subagent/tool-subagent/README.i18n.yaml | 4 +-
packages/subagent/tool-subagent/README.md | 8 +-
packages/subagent/tool-subagent/README.zh.md | 8 +-
packages/subagent/tool-subagent/src/index.ts | 23 +-
.../tool-subagent/tests/tool-subagent.spec.ts | 78 +++++++
60 files changed, 1168 insertions(+), 128 deletions(-)
create mode 100644 .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml
create mode 100644 .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
create mode 100644 .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md
create mode 100644 examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml
create mode 100644 examples/acp-agent/subagent-result-diagnostic.cordis.yml
create mode 100644 examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts
create mode 100644 examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/input.json
create mode 100644 examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/replay.override.json
create mode 100644 examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/session.jsonl
create mode 100644 examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/stdout.expected.jsonl
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
index 205757eacc..331a7a8f5d 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.i18n.yaml
+++ b/.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: a747d641ae112d114912958c289fe00b592e6ea5
-2026-08-10-product-subagent-providers-in-shared-host.zh.md: fef69e8a2d18135cbc9d5f0d80134fa5701bbbd0
+2026-08-10-product-subagent-providers-in-shared-host.md: dd5cd2b3b9c424da1f9f126d4ec9cb1fa4ca7083
+2026-08-10-product-subagent-providers-in-shared-host.zh.md: 0d946fa30240a130b27f85709382595cf29f5ead
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
index a747d641ae..dd5cd2b3b9 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
@@ -16,7 +16,7 @@ Product providers remain process-scoped host-plane registrations. The [productio
This note continues to own why a mounted product provider belongs on the host plane while its model-facing tool belongs to an Agent Preset. The production-install exclusion decision owns which Profiles install those optional packages. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
-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 providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, or test authentication. It may supply the mounted Provider's deployment configuration, including the Claude Code `permissionMode` owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving that choice into an Agent Preset or model-facing tool. Missing commands and product failures remain local to the attempted delegation.
Only a Profile that selects the Claude Code provider carries the Claude Agent SDK's optional platform CLI payload. Production still resolves the host `claude`; the SDK payload remains provider-package installation cost rather than the production executable.
diff --git a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
index fef69e8a2d..0d946fa302 100644
--- a/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.zh.md
@@ -16,7 +16,7 @@ Status: implemented
本说明继续负责解释为什么已经挂载的产品提供方属于 host plane,而面向模型的工具属于 Agent Preset。生产安装排除决策负责哪些 Profile 安装这些可选包。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
-这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
+这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本或测试身份验证。它可以提供已挂载 Provider 的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的 Claude Code `permissionMode`,但不会把该选择移入 Agent Preset 或面向模型的工具。命令缺失和产品故障仍局限于发生问题的那次委派。
只有选择 Claude Code 提供方的 Profile 才会携带 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷。生产环境仍解析宿主提供的 `claude`;这份 SDK 载荷是提供方包的安装成本,而不是生产可执行文件。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml
index 612916a290..bca41cb330 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.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/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
-2026-08-10-subagent-empty-terminal-message-output.md: 693013f6810005ce02b08bd82f1f6a18511c40fb
-2026-08-10-subagent-empty-terminal-message-output.zh.md: 64d61af21f838ef3f515db8af116cbdd74e96179
+2026-08-10-subagent-empty-terminal-message-output.md: 24bab01ad844a5b48e0bf6fe0fc54df6403f4bb7
+2026-08-10-subagent-empty-terminal-message-output.zh.md: ab3488806a4f7c019d783e563c79e06aeeb86f33
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
index 693013f681..24bab01ad8 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.md
@@ -12,7 +12,7 @@ The agent loop appends an empty-content `assistant/message` when a `max-tokens`
`dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty assistant message; without one, select the accumulated `text-delta` stream; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)` for session-event transports, `pushText(text)` for chunk-only transports, and `collect()` for selection. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
-The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message appends the child's partial text after the stop-reason headline so the parent model receives both the failure and available output.
+The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message presents the optional safe Provider diagnostic owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md) after the stop-reason headline and appends the child's partial text afterward. The parent model receives the failure, separate infrastructure detail, and available assistant output without conflating them.
## Verification
diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
index 64d61af21f..ab3488806a 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-10-subagent-empty-terminal-message-output.zh.md
@@ -12,7 +12,7 @@ Status: implemented
`dsh-subagent` 在 `src/assistant-output.ts` 中拥有唯一的规范选取规则:选取最后一条非空 assistant 消息;没有时选取累积的 `text-delta` 流;忽略空内容消息。增量的 `AssistantOutputFold` 通过 `push(event)` 处理会话事件传输,通过 `pushText(text)` 处理仅分片传输,并通过 `collect()` 完成选取。`finalAssistantOutput(events)` 把规则应用于完整的事件后缀,供进程内 `readResult` 与 Activation capture 使用。SDK 后端折叠通知事件;ACP 后端不暴露完整的 assistant 消息,而是折叠原始分片文本。`SubagentResult.output` 定义结果约定,`subagent/end.lastAssistantMessage` 使用同一规则。子 agent 不产生这两种输出中的任何一种时,一次性与 continuable 运行的生命周期字段都会缺省,而不是空数组。`max-tokens` 或 `aborted` 结果保留实际的终止原因。
-前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后附上子 agent 的部分文本,让父模型同时接收失败信息与已有输出。
+前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后呈现由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的可选安全提供方诊断,再附上子 agent 的部分文本。父模型会同时收到失败、独立的基础设施说明与已有 assistant 输出,而且不会把它们混为一体。
## 验证
diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml
index e8ece4d624..36fa7f01b2 100644
--- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.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-06-21-subagent-capability-seam.md
-2026-06-21-subagent-capability-seam.md: fa3b4f570bfccdc849a38b3eda16c1c8dd7b1827
-2026-06-21-subagent-capability-seam.zh.md: b25fe64377f98af92dbccb87f755627926975ef2
+2026-06-21-subagent-capability-seam.md: bc84d88d701a5f3018bf00f0ecf8b60750917407
+2026-06-21-subagent-capability-seam.zh.md: baec829a1a01018b490992fa0982bb23e43ba740
diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
index fa3b4f570b..bc84d88d70 100644
--- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
+++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
@@ -54,11 +54,11 @@ Fresh and forked children are separate providers, not a request flag. `dsh-subag
### Child isolation and the parent log
-Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output), while child steps and tool calls remain outside the parent log.
+Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output, or a failed result with optional provider diagnostic), while child steps and tool calls remain outside the parent log.
### Synchronous collect (first cut)
-`dsh-tool-subagent` passes its execution signal to `start()`, awaits the child result, and disposes the run before reporting. Non-completed outcomes become error results rather than successful partial output, and independent result and disposal rejections retain both diagnostics.
+`dsh-tool-subagent` passes its execution signal to `start()`, awaits the child result, and disposes the run before reporting. Non-completed outcomes become error results rather than successful partial output; they present the optional safe diagnostic owned by the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) separately from partial assistant text. Independent result and disposal rejections remain independently observable.
### Provider selection is config, not model-facing
diff --git a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md
index b25fe64377..baec829a1a 100644
--- a/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md
+++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.zh.md
@@ -54,11 +54,11 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
### 子 agent 隔离与父日志
-每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出),而子 agent 的步骤和工具调用均留在父日志之外。
+每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出,或带可选提供方诊断的失败结果),而子 agent 的步骤和工具调用均留在父日志之外。
### 同步收集(首版)
-`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;结果与 dispose 的拒绝相互独立,且两项诊断信息都会保留。
+`dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在报告前 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出;它会把由[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)负责的可选安全诊断与部分 assistant 文本分开呈现。结果与 dispose 的拒绝仍可彼此独立地观察。
### 提供方选择是配置,不面向模型
diff --git a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml
index 0c0cf829a3..715da62619 100644
--- a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.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-07-08-background-subagent-tasks.md
-2026-07-08-background-subagent-tasks.md: 412ec61dcdecae1a273c5993d25a4a099a22e864
-2026-07-08-background-subagent-tasks.zh.md: 9d108440c992e150ed62edaef8bf860813a471b8
+2026-07-08-background-subagent-tasks.md: 4dcd961ee5a5db925f8f6ad83e97890eaedb8e63
+2026-07-08-background-subagent-tasks.zh.md: dd8ba47e018bdeeaffccca1fdaf52df75728057b
diff --git a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
index 412ec61dcd..4dcd961ee5 100644
--- a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
+++ b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
@@ -8,7 +8,7 @@ English | [中文](2026-07-08-background-subagent-tasks.zh.md)
The [subagent seam](2026-06-21-subagent-capability-seam.md) returns a `SubagentRun`, but the model-facing tool originally collected every run synchronously. Independent, slow delegations therefore held the parent call open or ran serially.
-Subagents need the same start, collect, list, stop, ownership, notification, and cleanup behavior as other long-running tools without adopting process-stream semantics. The child session remains the detailed trace; the parent needs the final answer and job status. A background child also outlives its starting tool call, so its cancellation and owner-disposal contracts must be explicit.
+Subagents need the same start, collect, list, stop, ownership, notification, and cleanup behavior as other long-running tools without adopting process-stream semantics. The child session remains the detailed trace; the parent needs the final answer or safe failure detail plus job status. A background child also outlives its starting tool call, so its cancellation and owner-disposal contracts must be explicit.
## Decision
@@ -16,7 +16,7 @@ Each `dsh-tool-subagent` instance may expose `run_in_background`, controlled by
Background subagents use the [generic background job runtime](../architecture/2026-06-20-generic-long-running-tool-runtime.md). Collection, listing, cancellation, completion notices, and prompt guidance come from `job_output`, `job_list`, and `job_kill`; there are no subagent-specific companion tools.
-Foreground calls retain their synchronous contract: await provider startup and `run.result`, return final text only for `completed`, map other terminal reasons to an errored tool result, and always dispose the run before returning.
+Foreground calls retain their synchronous contract: await provider startup and `run.result`, return final text only for `completed`, map other terminal reasons to an errored tool result with the optional safe diagnostic described by the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md), and always dispose the run before returning.
For a background call, the tool validates the parent and refuses an already-aborted execution signal before calling `ctx.jobs.start()`. The job runtime preflights the control API and owner cleanup before invoking the producer starter. That starter creates an independent `AbortController` and begins `ctx.subagents.start()`; after the id is returned, the tool-call signal no longer owns the child.
@@ -24,7 +24,7 @@ The task registration maps the subagent seam as follows:
- `kind` is `subagent`, `label` is the model-supplied description, and `owner` is the parent agent.
- `cancel(reason?)` aborts the task-owned controller. The same signal covers pending provider startup and the published run's remaining work.
-- `done` awaits provider startup, the child result, and `run.dispose()`. Completed runs return final text, aborted runs become `killed`, and other stop reasons become `failed`. Startup, result, and disposal failures become failed outcomes rather than rejected task promises.
+- `done` awaits provider startup, the child result, and `run.dispose()`. Completed runs return final text, aborted runs become `killed`, and other stop reasons become `failed` with the Provider diagnostic when present. Startup, result, and disposal failures become failed outcomes rather than rejected task promises.
- `readOutput` is absent. While live, `job_output` returns status only; after settlement, it returns final output idempotently. Intermediate child activity remains in the child session.
## Lifecycle
diff --git a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
index 9d108440c9..dd8ba47e01 100644
--- a/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
@@ -8,7 +8,7 @@ Status: implemented
[subagent seam](2026-06-21-subagent-capability-seam.md) 会返回 `SubagentRun`,但原先面向模型的工具会同步收集每一次运行。因此,各自独立的慢速委派要么一直占用父调用,要么按串行方式运行。
-subagent 需要与其他长时间运行的工具相同的启动、收集、列出、停止、归属、通知和清理行为,但不应采用进程流语义。子会话仍是详细记录;父级只需最终答案和任务状态。后台子级的存活时间还会超过启动它的工具调用,因此必须明确其取消和拥有者资源释放约定。
+subagent 需要与其他长时间运行的工具相同的启动、收集、列出、停止、归属、通知和清理行为,但不应采用进程流语义。子会话仍是详细记录;父级只需最终答案或安全失败说明,以及任务状态。后台子级的存活时间还会超过启动它的工具调用,因此必须明确其取消和拥有者资源释放约定。
## 决策
@@ -16,7 +16,7 @@ subagent 需要与其他长时间运行的工具相同的启动、收集、列
后台 subagent 使用[通用后台任务运行时](../architecture/2026-06-20-generic-long-running-tool-runtime.md)。`job_output`、`job_list` 和 `job_kill` 负责收集、列出、取消、完成通知和提示词引导;系统不提供 subagent 专用的配套工具。
-前台调用保留其同步约定:等待提供方启动和 `run.result`;仅当状态为 `completed` 时返回最终文本;将其他终止原因映射为出错的工具结果;并且始终在返回前释放该运行。
+前台调用保留其同步约定:等待提供方启动和 `run.result`;仅当状态为 `completed` 时返回最终文本;将其他终止原因映射为出错的工具结果,并在存在时附上由[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)描述的可选安全诊断;而且始终在返回前释放该运行。
对于后台调用,工具会验证父级,并在调用 `ctx.jobs.start()` 前拒绝已中止的执行信号。任务运行时会在调用生产者启动器前,预检控制 API 和拥有者清理。该启动器创建独立的 `AbortController` 并启动 `ctx.subagents.start()`;返回 id 之后,工具调用的信号不再拥有该子级。
@@ -24,7 +24,7 @@ subagent 需要与其他长时间运行的工具相同的启动、收集、列
- `kind` 为 `subagent`,`label` 为模型提供的描述,`owner` 为父 agent(智能体)。
- `cancel(reason?)` 中止任务自有的控制器。同一个信号同时覆盖尚未完成的提供方启动和已发布 run 的剩余工作。
-- `done` 等待提供方启动、子级结果和 `run.dispose()`。已完成的运行返回最终文本,已中止的运行变为 `killed`,其他停止原因变为 `failed`。启动、结果和资源释放失败会转换为失败结果,而不是被拒绝的任务 Promise。
+- `done` 等待提供方启动、子级结果和 `run.dispose()`。已完成的运行返回最终文本,已中止的运行变为 `killed`,其他停止原因变为 `failed`,并在存在时携带提供方诊断。启动、结果和资源释放失败会转换为失败结果,而不是被拒绝的任务 Promise。
- `readOutput` 不存在。任务存活期间,`job_output` 只返回状态;结算后,它以幂等方式返回最终输出。中间的子级活动仍保留在子会话中。
## 生命周期
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
index 48773ba819..c642b870b8 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
+++ b/.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: 666945c4d8039874729a7f9da34d9cf82bfd479d
-2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 35b8dd51f1c45200a495c68f19f055a224123263
+2026-08-04-claude-code-and-codex-subagent-backends.md: d0e48bb2c048351f71687a66a31c8ecdda123328
+2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 3fd604927c447c24e9047424ab255eb9fd628226
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
index 666945c4d8..d0e48bb2c0 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
+++ b/.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 [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
+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 [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns Claude Code's Profile-selected mode and the shared failure diagnostic. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
@@ -50,9 +50,9 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
-The public configuration contains the same two deployment-owned values as the Codex sibling: an explicit `env` overlay and a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`. Each run creates its own `AbortController`, sets `persistSession: false`, and disables `AskUserQuestion`. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK rather than waiting for a user interface the provider does not own.
+The public configuration contains an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
-The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted`.
+The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. When a permission denial or unattended callback contributes to that failure, the result may additionally carry the bounded, non-assistant diagnostic owned by the non-interactive permissions decision. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted` without permission detail.
Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. `Query.close()` expresses graceful protocol intent but does not replace the subprocess owner's exit proof. Query-close failure, process failure, and teardown failure remain independently observable.
@@ -66,7 +66,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, an inherited interactive host setting overridden by the safe Provider mode, denied and bypassed writes in suite-owned temporary directories, safe permission diagnostics, process failure, local cancellation, whole-tree exit, and a real Windows batch shim under a path containing percent, ampersand, and exclamation metacharacters. This evidence proves the official SDK/CLI integration path, not compatibility with every independently installed product version. The Loader and shipped-profile evidence resolve both product packages by name while starting neither product, and the provider suite proves that the SDK receives the executable resolved from the host `PATH`.
The Claude Code 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.
@@ -82,7 +82,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
**Product doubles as required evidence.** Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.
-**Plugin-managed login, product home, models, settings, or permissions.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. The providers expose only an explicit environment overlay and teardown grace; unattended interaction fails closed.
+**Plugin-managed login, product home, models, settings, sandbox rules, or fine-grained permission policy.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. Claude Code exposes only one native non-interactive mode choice in addition to environment and teardown configuration; it does not mirror product rules or add a human interaction channel.
**Continuation, progress, product-native background state, and shared parent context.** The provider payload remains one final answer for one self-contained task. The generic Job layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt.
@@ -90,6 +90,6 @@ The project owner's distribution authorization is scoped to the official `@anthr
Users delegate through two stable one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
-Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, 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.
+Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed Claude Code run may separately expose the shared safe diagnostic. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, workspace settings, and selected Provider mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.
diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
index 35b8dd51f1..3fd604927c 100644
--- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
+++ b/.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`。本说明负责它们的产品协议、结果映射和进程生命周期;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
+harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责 Claude Code 的 Profile 模式选择与共享失败诊断。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs` 和 `dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
@@ -50,9 +50,9 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定名称 `claude`,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
-公开配置包含与 Codex 兄弟提供方相同、由部署方负责的两个值:显式的 `env` 覆盖项,以及须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
+公开配置包含显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
-只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`。
+只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。当权限拒绝或无人值守回调参与了该失败时,结果还可以携带由非交互权限决策负责的有界、非 assistant 诊断。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`,且不附带权限说明。
启动回滚和已发布运行的资源释放都会关闭 SDK query、中止该次运行的控制器、调用共享的进程树终止机制,并等待整棵进程树退出。`Query.close()` 表达优雅的协议关闭意图,但不能取代子进程责任方的退出证明。Query 关闭失败、进程失败和清理失败仍可彼此独立地观察。
@@ -66,7 +66,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`、原始任务、逐字节完全一致的最终回答、安全提供方模式对继承的交互式宿主设置的覆盖、测试所拥有临时目录中的拒绝写入与 bypass 写入、安全权限诊断、进程失败、本地取消、整棵进程树退出,以及位于同时含百分号、与号和感叹号路径中的真实 Windows batch shim。这项证据证明官方 SDK/CLI 集成路径,而不证明它与每个独立安装的产品版本兼容。Loader 与随附 profile 证据会按名称解析两个产品包且不启动产品,provider 测试则证明 SDK 收到由宿主 `PATH` 解析出的可执行文件。
带密钥 Claude Code e2e 仅在提供方的内存环境中映射密钥与固定的官方端点,把模型变量设为文档所示的 `deepseek-v4-pro[1m]` 与 `deepseek-v4-flash`,并实际经过生产提供方、官方 SDK 与真实 CLI。它将去除首尾空白后的结果与一个随机数比较,并证明整棵进程树退出,且测试不会直接调用 Messages API。
@@ -82,7 +82,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture。
-**由插件管理登录、产品主目录、模型、设置或权限。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。提供方只公开显式环境覆盖项和清理宽限期;无人值守交互会以默认拒绝方式失败。
+**由插件管理登录、产品主目录、模型、设置、沙箱规则或细粒度权限策略。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。Claude Code 除环境和清理配置外只公开一个原生非交互模式选择;它不会镜像产品规则,也不会增加人工交互通道。
**续接、进度、产品原生后台状态和共享父级上下文。** 提供方载荷仍是一项自包含任务的一个最终回答。通用 Job 层可以额外提供 id、状态、通知、收集与取消结果,但产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和提供方专属后台状态都需要独立的用户约定,当前实现不会预先构建这些功能。
@@ -90,6 +90,6 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
用户通过官方产品集成支持的两个稳定一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
-每次委派都要承担新建产品进程和独立模型上下文的开销。到达父级的产品载荷仍只有最终文本;后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
+每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的 Claude Code 运行可以另行公开共享安全诊断。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态、工作区设置和所选提供方模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。
diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml
index 2310e148a0..b8ef147519 100644
--- a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.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-12-product-subagent-one-shot-background-tasks.md
-2026-08-12-product-subagent-one-shot-background-tasks.md: b8865cf94852396c32dd6da996bc9f5c2c7d806b
-2026-08-12-product-subagent-one-shot-background-tasks.zh.md: bbc18ebb6cf0de1b04f0c2a10ddf51cea282c24e
+2026-08-12-product-subagent-one-shot-background-tasks.md: e389c0b8b6587cf699ea3fd30e75531bb6069108
+2026-08-12-product-subagent-one-shot-background-tasks.zh.md: d424fa9d1ccb1f14fa73e342964e95b7181c8274
diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md
index b8865cf948..e389c0b8b6 100644
--- a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md
+++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md
@@ -14,9 +14,9 @@ Exposing background execution must not add a product session, product-specific j
Production `dsh` does not install the optional product providers. A Profile that opts in installs and mounts `dsh-subagent-codex`, `dsh-subagent-claude-code`, or both once on the host plane. The `standard`, `code`, and `cordis` Agent Presets configure the corresponding dormant tool rows with `backgroundMode: one-shot`; removing a row's `disabled` field exposes the existing optional `run_in_background` argument to agents composed from that preset. Omission or `false` waits in the foreground; explicit `true` returns a parent-owned Job id after synchronous Job preflight and registration, without waiting for provider startup or completion.
-The [generic one-shot background adapter](2026-07-08-background-subagent-tasks.md) owns background registration and settlement. It starts the same [`SubagentRun`](2026-06-21-subagent-capability-seam.md), uses a Job-owned cancellation signal across provider startup and execution, waits for `run.result` and `run.dispose()`, maps the terminal result into the Job, and lets `job_output`, `job_list`, `job_kill`, and the existing completion notice expose that state. The [product provider decision](2026-08-04-claude-code-and-codex-subagent-backends.md) continues to own native protocols, answer selection, local cancellation, and process-tree quiescence.
+The [generic one-shot background adapter](2026-07-08-background-subagent-tasks.md) owns background registration and settlement. It starts the same [`SubagentRun`](2026-06-21-subagent-capability-seam.md), uses a Job-owned cancellation signal across provider startup and execution, waits for `run.result` and `run.dispose()`, maps the terminal result and optional safe diagnostic into the Job, and lets `job_output`, `job_list`, `job_kill`, and the existing completion notice expose that state. The [product provider decision](2026-08-04-claude-code-and-codex-subagent-backends.md) continues to own native protocols, answer selection, local cancellation, and process-tree quiescence; the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns Claude Code's Profile configuration and diagnostic production.
-No provider configuration, service interface, event, wire field, persistence format, or product identifier is added. Foreground and background differ only in which existing consumer waits for the same one-shot run.
+This scheduling decision adds no provider configuration, service interface, event, wire field, persistence format, or product identifier. A Provider may define its own Profile configuration independently; foreground and background still differ only in which existing consumer waits for the same one-shot run.
### Ownership and lifecycle
@@ -37,7 +37,7 @@ product tool call
| Product selection and exposure | Agent Preset | Bind one fixed tool name to one fixed provider | Enabling one row exposes only that product tool |
| Foreground or background choice | `dsh-tool-subagent` | Resolve `run_in_background` under `one-shot` policy | Omission is foreground; explicit `true` returns a Job id |
| Job id, state, output, cancellation, and notice | `ctx.jobs` and `dsh-tool-jobs` | Register and present the existing one-shot run | Generic job tools collect or stop the run for the exact parent |
-| Native answer and process quiescence | Product provider and `dsh-subprocess` | Produce one final result and release one process tree | Job settlement and foreground return both wait for disposal |
+| Native result, optional diagnostic, and process quiescence | Product provider and `dsh-subprocess` | Produce one final result and release one process tree | Job settlement and foreground return consume the same result and both wait for disposal |
## Published composition
@@ -49,7 +49,7 @@ The ACP product compositions use the same fixed product rows and generic job con
## Verification
-The Web composition test explicitly mounts both optional providers from the repository examples dependency anchor, then boots four user-preset variants—neither product, Codex, Claude Code, and both—and checks that each enabled product tool exposes `run_in_background` alongside `job_output`, `job_list`, and `job_kill`. The two package-owned Loader compositions run with an empty `PATH`, inspect the same schemas and controls, and prove that explicit provider loading starts no product process. ACP keyless snapshots pin the assembled explicit product schemas, while the existing `dsh-tool-subagent` and job suites pin foreground defaulting, Job registration, final-output collection, cancellation, completion notices, owner disposal, and provider disposal.
+The Web composition test explicitly mounts both optional providers from the repository examples dependency anchor, then boots four user-preset variants—neither product, Codex, Claude Code, and both—and checks that each enabled product tool exposes `run_in_background` alongside `job_output`, `job_list`, and `job_kill`. The two package-owned Loader compositions run with an empty `PATH`, inspect the same schemas and controls, and prove that explicit provider loading starts no product process. ACP keyless snapshots pin the assembled explicit product schemas, while the existing `dsh-tool-subagent` and job suites pin foreground defaulting, Job registration, final-output collection, shared diagnostic presentation, cancellation, completion notices, owner disposal, and provider disposal.
## Alternatives considered
@@ -65,6 +65,6 @@ The Web composition test explicitly mounts both optional providers from the repo
## Consequences
-Agents can continue useful work while Codex or Claude Code handles an independent one-shot task, then collect the final answer or cancel it through the same Job controls used by other background producers. Foreground callers retain their existing result and error behavior.
+Agents can continue useful work while Codex or Claude Code handles an independent one-shot task, then collect the final answer or cancel it through the same Job controls used by other background producers. Foreground and one-shot background consumers present the same safe Provider diagnostic when a failed result supplies one.
-Every product delegation still starts a fresh native process or query, produces final text as its only product payload, and ends with provider disposal and whole-tree exit. A background call additionally exposes the generic Job id, status, completion notice, and collection or cancellation results. Background Jobs are process-local and parent-owned: they do not survive parent disposal, do not expose intermediate product activity, and do not make a product conversation resumable. Production installs do not pay for either product integration unless a Profile explicitly installs it; any composition that exposes the background argument must also keep the generic Job provider and controls available.
+Every product delegation still starts a fresh native process or query, produces final assistant text as its only assistant payload, and ends with provider disposal and whole-tree exit. A failed result may separately carry a safe diagnostic. A background call additionally exposes the generic Job id, status, completion notice, and collection or cancellation results. Background Jobs are process-local and parent-owned: they do not survive parent disposal, do not expose intermediate product activity, and do not make a product conversation resumable. Production installs do not pay for either product integration unless a Profile explicitly installs it; any composition that exposes the background argument must also keep the generic Job provider and controls available.
diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md
index bbc18ebb6c..d424fa9d1c 100644
--- a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md
@@ -14,9 +14,9 @@ Codex 与 Claude Code 提供方已经能够运行一项自包含任务并返回
生产 `dsh` 不安装可选产品提供方。选择启用产品集成的 Profile 会安装 `dsh-subagent-codex`、`dsh-subagent-claude-code` 或两者,并在 host plane(宿主平面)各挂载一次。`standard`、`code` 与 `cordis` Agent Preset 使用 `backgroundMode: one-shot` 配置相应的休眠工具行;删除某一行的 `disabled` 字段后,现有可选参数 `run_in_background` 会向由该 preset 组装的 agent 公开。省略该参数或传入 `false` 时会在前台等待;显式传入 `true` 时会在同步完成 Job 预检与登记后返回由父级拥有的 Job id,而不会等待提供方启动或完成。
-[通用 one-shot 后台适配器](2026-07-08-background-subagent-tasks.md)负责后台登记与结算。它会启动同一个 [`SubagentRun`](2026-06-21-subagent-capability-seam.md),让 Job 自有的取消信号覆盖提供方启动与执行,等待 `run.result` 和 `run.dispose()`,把终态结果映射进 Job,并由 `job_output`、`job_list`、`job_kill` 与现有完成通知公开该状态。[产品提供方决策](2026-08-04-claude-code-and-codex-subagent-backends.md)继续负责原生协议、答案选择、本地取消与进程树完全停稳。
+[通用 one-shot 后台适配器](2026-07-08-background-subagent-tasks.md)负责后台登记与结算。它会启动同一个 [`SubagentRun`](2026-06-21-subagent-capability-seam.md),让 Job 自有的取消信号覆盖提供方启动与执行,等待 `run.result` 和 `run.dispose()`,把终态结果与可选安全诊断映射进 Job,并由 `job_output`、`job_list`、`job_kill` 与现有完成通知公开该状态。[产品提供方决策](2026-08-04-claude-code-and-codex-subagent-backends.md)继续负责原生协议、答案选择、本地取消与进程树完全停稳;[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)负责 Claude Code 的 Profile 配置与诊断生产。
-本决策不新增提供方配置、服务接口、事件、协议字段、持久化格式或产品标识符。前台与后台的区别仅在于由哪个现有消费方等待同一个 one-shot 运行。
+本调度决策不新增提供方配置、服务接口、事件、协议字段、持久化格式或产品标识符。提供方可以独立定义自己的 Profile 配置;前台与后台的区别仍然只在于由哪个现有消费方等待同一个 one-shot 运行。
### 归属与生命周期
@@ -37,7 +37,7 @@ product tool call
| 产品选择与公开 | Agent Preset | 把一个固定工具名绑定到一个固定提供方 | 启用一行只会公开对应产品工具 |
| 前台或后台选择 | `dsh-tool-subagent` | 按 `one-shot` 策略解析 `run_in_background` | 省略参数时在前台运行;显式传入 `true` 时返回 Job id |
| Job id、状态、输出、取消与通知 | `ctx.jobs` 与 `dsh-tool-jobs` | 登记并展示现有 one-shot 运行 | 通用作业工具为准确父级收集或停止运行 |
-| 原生答案与进程完全停稳 | 产品提供方与 `dsh-subprocess` | 产生一个最终结果并释放一棵进程树 | Job 结算与前台返回都会等待资源释放 |
+| 原生结果、可选诊断与进程完全停稳 | 产品提供方与 `dsh-subprocess` | 产生一个最终结果并释放一棵进程树 | Job 结算与前台返回消费同一结果,且都会等待资源释放 |
## 发布组装
@@ -49,7 +49,7 @@ ACP 产品组装使用相同的固定产品行与通用作业控制工具。其
## 验证
-Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供方,再启动四种用户 preset 变体——不启用产品、只启用 Codex、只启用 Claude Code,以及同时启用两者——并检查每个已启用产品工具都会与 `job_output`、`job_list` 和 `job_kill` 一起公开 `run_in_background`。两个由包负责的 Loader 组装会在空 `PATH` 下运行,检查相同 schema 与控制工具,并证明显式加载提供方不会启动产品进程。ACP 无密钥快照会固定显式组装后的产品 schema,而现有 `dsh-tool-subagent` 与作业测试套件会固定前台默认值、Job 登记、最终输出收集、取消、完成通知、owner 资源释放与提供方资源释放。
+Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供方,再启动四种用户 preset 变体——不启用产品、只启用 Codex、只启用 Claude Code,以及同时启用两者——并检查每个已启用产品工具都会与 `job_output`、`job_list` 和 `job_kill` 一起公开 `run_in_background`。两个由包负责的 Loader 组装会在空 `PATH` 下运行,检查相同 schema 与控制工具,并证明显式加载提供方不会启动产品进程。ACP 无密钥快照会固定显式组装后的产品 schema,而现有 `dsh-tool-subagent` 与作业测试套件会固定前台默认值、Job 登记、最终输出收集、共享诊断呈现、取消、完成通知、owner 资源释放与提供方资源释放。
## 曾考虑的替代方案
@@ -65,6 +65,6 @@ Web 组装测试会从仓库 examples 依赖锚点显式挂载两个可选提供
## 后果
-agent 可以在 Codex 或 Claude Code 处理独立 one-shot 任务时继续推进其他工作,随后通过其他后台 producer 共用的 Job 控制工具收集最终回答或取消运行。前台调用方继续获得既有结果与错误行为。
+agent 可以在 Codex 或 Claude Code 处理独立 one-shot 任务时继续推进其他工作,随后通过其他后台 producer 共用的 Job 控制工具收集最终回答或取消运行。若失败结果提供了安全的提供方诊断,前台与一次性后台消费方会呈现同一内容。
-每次产品委托仍会启动一个全新的原生进程或 query,把最终文本作为唯一产品载荷,并以提供方资源释放和整棵进程树退出结束。后台调用还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。后台 Job 仅存在于当前进程且由父级拥有:它不会在父级资源释放后继续存活,不会公开产品中间活动,也不会让产品对话变得可恢复。只有 Profile 显式安装产品集成时,生产安装才承担对应成本;公开后台参数的任何组装还必须让通用 Job 提供方与控制工具保持可用。
+每次产品委托仍会启动一个全新的原生进程或 query,把最终 assistant 文本作为唯一 assistant 载荷,并以提供方资源释放和整棵进程树退出结束。失败结果可以另行携带安全诊断。后台调用还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。后台 Job 仅存在于当前进程且由父级拥有:它不会在父级资源释放后继续存活,不会公开产品中间活动,也不会让产品对话变得可恢复。只有 Profile 显式安装产品集成时,生产安装才承担对应成本;公开后台参数的任何组装还必须让通用 Job 提供方与控制工具保持可用。
diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml
new file mode 100644
index 0000000000..42e26c2f7e
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
+2026-08-15-product-subagent-noninteractive-permissions.md: f382bc7ad058fefd8001da6181824fc9b6f767d4
+2026-08-15-product-subagent-noninteractive-permissions.zh.md: 76cf53c7c9af791db6e54a8b779a7284187d0716
diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
new file mode 100644
index 0000000000..f382bc7ad0
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
@@ -0,0 +1,72 @@
+# Agent Note: Claude Code subagents use Profile-selected non-interactive permissions
+
+Status: implemented
+
+English | [中文](2026-08-15-product-subagent-noninteractive-permissions.zh.md)
+
+## Problem
+
+The [Claude Code product provider](2026-08-04-claude-code-and-codex-subagent-backends.md) runs without a human interface. Native permission prompts, user dialogs, or MCP elicitation therefore cannot wait for a person, but relying on the product's ambient default can still select an interactive mode. A deployment also needs to choose broader native modes without giving the parent model or one tool call a way to raise its own authority.
+
+A failed product run previously reached the [subagent seam](2026-06-21-subagent-capability-seam.md) only as a stop reason. Logs could retain the product error, but the foreground parent and a [one-shot background Job](2026-08-12-product-subagent-one-shot-background-tasks.md) could not distinguish a permission refusal from another failure. Reusing assistant output for that fact would misattribute infrastructure detail to the child model.
+
+## Decision
+
+The Claude Code Provider owns one Profile-level `permissionMode` value. It defaults to `dontAsk` and accepts only the native non-interactive modes supported by the pinned Agent SDK:
+
+| Value | Native behavior |
+| --- | --- |
+| `dontAsk` | Deny operations that are not already authorized instead of prompting. |
+| `acceptEdits` | Accept edits; deny any remaining permission prompt through the unattended callback. |
+| `auto` | Let Claude Code's native classifier allow or deny permission requests. |
+| `plan` | Use Claude Code's planning-only mode without tool execution. |
+| `bypassPermissions` | Set the SDK's explicit dangerous confirmation and bypass permission checks. |
+
+The Provider fixes the resolved value for every run from that plugin instance. The subagent tool schema and `SubagentStartRequest` contain no permission field, so a model or individual delegation cannot change it. The Provider continues to omit `settingSources`: Claude Code remains the owner of user, project, and local settings, authentication, tools, and sandbox behavior outside the selected mode.
+
+Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny instead of returning the SDK's indefinitely blocking `null`; MCP elicitation is declined; the supported refusal dialog is cancelled; undeclared dialog kinds use the SDK's no-dialog failure behavior. A native `permission_denied` message records the same operation-local fact. These paths do not create an approval session, queue, cache, or retry loop.
+
+### Failure diagnostic
+
+`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character.
+
+Claude Code records only the effective mode, request category, unattended decision, and a fixed safe reason. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. When a permission fact contributes to a published run that settles as `error`, the Provider attaches the diagnostic without adding it to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
+
+The foreground consumer presents the stop-reason headline, then the optional diagnostic, then any partial assistant output. The one-shot background adapter stores the same diagnostic beside the stop reason in the failed Job detail. Providers that omit the field retain their previous behavior.
+
+### Ownership and lifecycle
+
+| Fact or resource | Owner | Observable behavior |
+| --- | --- | --- |
+| Profile permission choice | Claude Code Provider Config | Invalid, interactive, or unknown values fail during configuration. |
+| Permission and sandbox semantics | Claude Code and its Agent SDK | The Provider passes one native mode and does not mirror product policy. |
+| Interaction decisions and safe diagnostic | One Claude Code run | Concurrent runs keep independent mode, callback, and diagnostic state. |
+| Diagnostic type and byte limit | `dsh-subagent` | Consumers receive a bounded optional field separate from assistant output. |
+| Foreground and Job presentation | `dsh-tool-subagent` and the generic Job runtime | Scheduling choice does not change the underlying failure fact. |
+| Process cancellation and quiescence | Product Provider and `dsh-subprocess` | Result settlement still precedes idempotent whole-tree disposal. |
+
+## Verification
+
+Package tests pin every allowed and rejected Config value, the exact SDK option mapping, bypass confirmation, callback terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, and disposal behavior. The real Agent SDK/CLI fixture proves that the default overrides an interactive native setting, denies an out-of-workspace write with safe diagnostic detail, executes an explicit bypass write only inside suite-owned temporary storage, and leaves the full process tree quiescent. Loader composition proves a non-default mode can be published without starting either product, and the keyless ACP snapshot records the same diagnostic in a foreground tool error and one-shot `job_output` while the model-facing product tool schema contains no permission parameter.
+
+## Alternatives considered
+
+**Use the product's ambient permission default.** A native setting may select an interactive mode and make unattended behavior deployment-dependent. The Provider must choose a non-interactive mode explicitly for every query.
+
+**Put permission mode in the model-facing tool or each start request.** That would let task content select authority and would duplicate a Profile deployment decision on every call.
+
+**Copy Claude settings or map the parent Harness sandbox.** The products do not share one permission vocabulary. Mirroring their state would create a second authority and obscure the native sandbox consequences of `auto` and bypass modes.
+
+**Forward prompts to a parent, Web client, or CLI.** The one-shot product run has no owned human-interaction lifecycle. Adding one would require durable request identity, routing, cancellation, and timeout semantics beyond this decision.
+
+**Return raw product errors, stderr, or tool inputs.** Those values can contain commands, paths, workspace data, environment values, or credentials. A fixed safe diagnostic keeps the failure actionable without exposing the product transcript.
+
+**Store a separate Job diagnostic.** The Job is only a scheduling adapter for the same `SubagentRun`; a second field would let foreground and background failure meanings drift.
+
+## Consequences
+
+Profiles can select Claude Code's native restricted, automatic, planning, edit-accepting, or bypass behavior before the Provider starts, while the safe default never asks a person. Broader modes remain explicit deployment choices and retain their native sandbox consequences.
+
+Permission failures become visible to both foreground parents and one-shot background Jobs without turning infrastructure text into an assistant answer. That diagnostic can enter model context, Job notices, API projections, and Job UI through the ordinary consumer paths, so the Provider must sanitize and bound it before result settlement.
+
+The change adds no product session persistence, human approval channel, dynamic permission operation, progress stream, retry policy, or rollback. Codex and other Providers remain valid without producing a diagnostic or exposing a permission-mode Config.
diff --git a/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md
new file mode 100644
index 0000000000..76cf53c7c9
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md
@@ -0,0 +1,72 @@
+# Agent Note: Claude Code subagent 使用 Profile 选择的非交互权限
+
+Status: implemented
+
+[English](2026-08-15-product-subagent-noninteractive-permissions.md) | 中文
+
+## Problem
+
+[Claude Code 产品提供方](2026-08-04-claude-code-and-codex-subagent-backends.md)在没有人工界面的情况下运行。因此,原生权限提示、用户对话或 MCP elicitation 不能等待人员响应,但依赖产品环境中的默认值仍可能选择交互模式。部署也需要选择更宽松的原生模式,同时不能让父模型或单次工具调用提升自身权限。
+
+失败的产品运行此前只能把终止原因送入 [subagent seam](2026-06-21-subagent-capability-seam.md)。日志可以保留产品错误,但前台父 agent 与[一次性后台 Job](2026-08-12-product-subagent-one-shot-background-tasks.md)无法区分权限拒绝和其他失败。若复用 assistant 输出承载该事实,则会把基础设施说明错误归因给子模型。
+
+## Decision
+
+Claude Code 提供方拥有一个 Profile 级 `permissionMode` 值。它默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支持的原生非交互模式:
+
+| 值 | 原生行为 |
+| --- | --- |
+| `dontAsk` | 不弹出提示,直接拒绝尚未获授权的操作。 |
+| `acceptEdits` | 接受编辑;其余权限提示由无人值守回调拒绝。 |
+| `auto` | 由 Claude Code 原生分类器允许或拒绝权限请求。 |
+| `plan` | 使用 Claude Code 的仅规划模式,不执行工具。 |
+| `bypassPermissions` | 设置 SDK 的显式危险确认并跳过权限检查。 |
+
+提供方会为该插件实例的每次运行固定已解析值。subagent 工具 schema 与 `SubagentStartRequest` 都不包含权限字段,因此模型或单次委派无法改变它。提供方继续省略 `settingSources`:除所选模式以外,用户、项目和本地设置、身份验证、工具与沙箱行为仍由 Claude Code 拥有。
+
+每次 query 都禁用 `AskUserQuestion`。非 bypass 模式的权限回调会拒绝请求,而不会返回 SDK 中会无限阻塞的 `null`;MCP elicitation 会被拒绝;已支持的拒绝对话会被取消;未声明的对话类型使用 SDK 的无对话失败行为。原生 `permission_denied` 消息会记录同一份当前运行事实。这些路径不会创建审批会话、队列、缓存或重试循环。
+
+### 失败诊断
+
+`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。
+
+Claude Code 只记录有效模式、请求类别、无人值守决定与固定的安全原因。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。当一项权限事实参与了已经发布、最终以 `error` 结算的运行时,提供方会附加诊断,但不会把它写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`。
+
+前台消费方依次呈现终止原因标题、可选诊断和任何部分 assistant 输出。一次性后台适配器会在失败 Job 的 detail 中,把同一诊断与终止原因一起保存。没有填写该字段的提供方保持原有行为。
+
+### 所有权与生命周期
+
+| 事实或资源 | Owner | 可观察行为 |
+| --- | --- | --- |
+| Profile 权限选择 | Claude Code 提供方 Config | 配置阶段会拒绝无效、交互式或未知值。 |
+| 权限与沙箱语义 | Claude Code 及其 Agent SDK | 提供方传入一个原生模式,不镜像产品策略。 |
+| 交互决定与安全诊断 | 单次 Claude Code 运行 | 并发运行分别拥有独立的模式、回调与诊断状态。 |
+| 诊断类型与字节上限 | `dsh-subagent` | 消费方收到与 assistant 输出分离的有界可选字段。 |
+| 前台与 Job 呈现 | `dsh-tool-subagent` 和通用 Job 运行时 | 调度选择不会改变底层失败事实。 |
+| 进程取消与完全停稳 | 产品提供方和 `dsh-subprocess` | 结果结算后仍执行幂等的完整进程树资源释放。 |
+
+## Verification
+
+包测试固定所有允许与拒绝的 Config 值、准确的 SDK 选项映射、bypass 确认、回调终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail 和资源释放行为。真实 Agent SDK/CLI fixture 证明默认值会覆盖交互式原生设置,越出工作区的写入会被拒绝并返回安全诊断,显式 bypass 写入只会发生在测试拥有的临时存储中,而且完整进程树会完全停稳。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录同一诊断如何出现在前台工具错误与一次性 `job_output` 中,同时面向模型的产品工具 schema 不包含权限参数。
+
+## Alternatives considered
+
+**使用产品环境中的权限默认值。** 原生设置可能选择交互模式,使无人值守行为依赖部署环境。提供方必须为每次 query 显式选择非交互模式。
+
+**把权限模式放入面向模型的工具或每次 start 请求。** 这会让任务内容选择权限,并在每次调用中重复一个 Profile 部署决定。
+
+**复制 Claude 设置或映射父级 Harness 沙箱。** 各产品并不共享同一套权限词汇。镜像这些状态会创建第二个权威,并掩盖 `auto` 与 bypass 模式的原生沙箱后果。
+
+**把提示转发给父 agent、Web 客户端或 CLI。** 一次性产品运行没有由其拥有的人工交互生命周期。新增该能力需要持久请求身份、路由、取消与 timeout 语义,超出本决策范围。
+
+**返回原始产品错误、stderr 或工具输入。** 这些值可能包含命令、路径、工作区数据、环境值或凭证。固定的安全诊断既保留可操作性,也不会暴露产品 transcript。
+
+**单独保存 Job 诊断。** Job 只是同一 `SubagentRun` 的调度适配器;第二个字段会让前台和后台的失败含义发生漂移。
+
+## Consequences
+
+Profile 可以在提供方启动前选择 Claude Code 原生的受限、自动、仅规划、编辑放行或 bypass 行为,而安全默认值绝不会询问人员。更宽松的模式仍是显式部署选择,并保留其原生沙箱后果。
+
+权限失败会同时到达前台父 agent 和一次性后台 Job,且不会把基础设施文本伪装成 assistant 回答。该诊断可以沿普通消费路径进入模型上下文、Job 通知、API 投影与 Job UI,因此提供方必须在结果结算前完成脱敏和限长。
+
+本改动不增加产品会话持久化、人工审批通道、动态权限操作、进度流、重试策略或回滚。Codex 与其他提供方无需产生诊断或公开权限模式 Config,仍然保持合法。
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index ef4931f765..921934f9ac 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/docs/config-catalog.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/config-catalog.md
-config-catalog.md: 82f6d26c79d32c6952f3bc11c96fa1c2ddceecdc
-config-catalog.zh.md: 958d3115447db37de248bbf30b0744308ff8dbb8
+config-catalog.md: 8294c2187f2b80fbf36787c784ad8b73a16206c1
+config-catalog.zh.md: f35392a5b005212067c9b593b7fa2818202466dd
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 82f6d26c79..8294c2187f 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -2081,19 +2081,29 @@ Source: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/
Requires: `subagents` · `subprocess`
```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
export interface Config {
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
*/
env?: Record
+ /** Native non-interactive permission mode fixed for this Provider instance. */
+ permissionMode?: ClaudeCodePermissionMode
/** Grace in milliseconds for Claude Code process-tree termination. */
disposeGraceMs?: number
}
+
+/** Profile-selectable non-interactive Claude Code permission mode. */
+export type ClaudeCodePermissionMode =
+ | 'dontAsk'
+ | 'acceptEdits'
+ | 'auto'
+ | 'plan'
+ | 'bypassPermissions'
```
-Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+Source: [`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 958d311544..f35392a5b0 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -2083,19 +2083,29 @@ export type PermissionPolicy = 'allow' | 'reject'
需要:`subagents` · `subprocess`
```ts config-catalog
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
export interface Config {
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
*/
env?: Record
+ /** Native non-interactive permission mode fixed for this Provider instance. */
+ permissionMode?: ClaudeCodePermissionMode
/** Grace in milliseconds for Claude Code process-tree termination. */
disposeGraceMs?: number
}
+
+/** Profile-selectable non-interactive Claude Code permission mode. */
+export type ClaudeCodePermissionMode =
+ | 'dontAsk'
+ | 'acceptEdits'
+ | 'auto'
+ | 'plan'
+ | 'bypassPermissions'
```
-来源:[`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+来源:[`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml
index a86dc8de4a..0770ded7f6 100644
--- a/docs/subsystems/subagent.i18n.yaml
+++ b/docs/subsystems/subagent.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/subsystems/subagent.md
-subagent.md: a683a679e6017351540ee4b73adc74375ef0a1d6
-subagent.zh.md: 61391cd297c0eb14f4c0d8eac4539b551cb60bda
+subagent.md: 9a21cecce9144c3aa4c268d753c0aeff5f3ac178
+subagent.zh.md: 4a487fd655f45622bedaa224bad332d6c9ae3ace
diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md
index a683a679e6..9a21cecce9 100644
--- a/docs/subsystems/subagent.md
+++ b/docs/subsystems/subagent.md
@@ -307,7 +307,7 @@ type SubagentDescendantListEntry = SubagentListEntry & {
## The terminal result: `SubagentResult`
-The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is present only after a requested `outputSchema` was successfully satisfied; requesting a schema does not guarantee it, and a provider may return `stopReason: 'error'` when the child fails or finishes without a valid capture. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success.
+The outcome of a one-shot run, resolved by `SubagentRun.result`. `structured` is present only after a requested `outputSchema` was successfully satisfied; requesting a schema does not guarantee it, and a provider may return `stopReason: 'error'` when the child fails or finishes without a valid capture. A provider may attach a safe, non-assistant `diagnostic` to a non-`completed` result; the provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads and limits the complete value to 4096 UTF-8 bytes before consumers present it separately from `output`. A non-`completed` `stopReason` means `output` may be partial — the consumer maps it to an `isError` tool result rather than reporting partial output as success.
```ts type-equiv
/**
@@ -330,6 +330,13 @@ interface SubagentResult {
* schema-agnostic.
*/
readonly structured?: unknown
+ /**
+ * Provider-authored, non-assistant failure detail for a non-`completed`
+ * result. Providers keep this text free of tool inputs, file contents,
+ * environment values, credentials, and raw protocol payloads, and limit it
+ * to 4096 UTF-8 bytes. Consumers present it separately from {@link output}.
+ */
+ readonly diagnostic?: string
/** Why the run ended. A non-`completed` reason means `output` may be partial. */
readonly stopReason: SubagentStopReason
}
diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md
index 61391cd297..4a487fd655 100644
--- a/docs/subsystems/subagent.zh.md
+++ b/docs/subsystems/subagent.zh.md
@@ -307,7 +307,7 @@ type SubagentDescendantListEntry = SubagentListEntry & {
## 终态结果:`SubagentResult`
-单次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
+单次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。提供方可以为非 `completed` 结果附带安全且不属于 assistant 内容的 `diagnostic`;在消费方将它与 `output` 分开呈现前,提供方会排除工具输入、文件内容、环境值、凭证与原始协议载荷,并把完整值限制在 4096 个 UTF-8 字节以内。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
```ts type-equiv
/**
@@ -330,6 +330,13 @@ interface SubagentResult {
* schema-agnostic.
*/
readonly structured?: unknown
+ /**
+ * Provider-authored, non-assistant failure detail for a non-`completed`
+ * result. Providers keep this text free of tool inputs, file contents,
+ * environment values, credentials, and raw protocol payloads, and limit it
+ * to 4096 UTF-8 bytes. Consumers present it separately from {@link output}.
+ */
+ readonly diagnostic?: string
/** Why the run ended. A non-`completed` reason means `output` may be partial. */
readonly stopReason: SubagentStopReason
}
diff --git a/examples/acp-agent/product-subagent-both.cordis.snapshot.yml b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml
index 0f8760cb91..39f464a6b7 100644
--- a/examples/acp-agent/product-subagent-both.cordis.snapshot.yml
+++ b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml
@@ -22,6 +22,8 @@
name: '@deepseek-ai/dsh-subagent-codex'
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
+ config:
+ permissionMode: acceptEdits
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
config:
diff --git a/examples/acp-agent/product-subagent-both.cordis.yml b/examples/acp-agent/product-subagent-both.cordis.yml
index 6a75bec332..6c5154fc6b 100644
--- a/examples/acp-agent/product-subagent-both.cordis.yml
+++ b/examples/acp-agent/product-subagent-both.cordis.yml
@@ -11,6 +11,8 @@
name: '@deepseek-ai/dsh-subagent-codex'
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
+ config:
+ permissionMode: acceptEdits
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
config:
diff --git a/examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml b/examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml
new file mode 100644
index 0000000000..563f7d6864
--- /dev/null
+++ b/examples/acp-agent/subagent-result-diagnostic.cordis.snapshot.yml
@@ -0,0 +1,29 @@
+# Keyless twin of subagent-result-diagnostic.cordis.yml: keep the same test
+# provider/tool and replace only the external model adapter.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - insert:
+ - id: llm-replay
+ name: '@deepseek-ai/dsh-llm-replay'
+ config:
+ providers:
+ - id: deepseek-official
+ name: DeepSeek
+ models:
+ - id: deepseek-v4-flash
+ - id: deepseek-v4-pro
+ - id: subagent-result-diagnostic
+ name: './tests/fixtures/subagent-result-diagnostic.ts'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: snapshot-diagnostic
+ toolName: subagent_codex
+ backgroundMode: one-shot
+ maxDepth: provider-managed
+ - id: llm-deepseek
+ name: '@deepseek-ai/dsh-llm-deepseek'
+ disabled: true
diff --git a/examples/acp-agent/subagent-result-diagnostic.cordis.yml b/examples/acp-agent/subagent-result-diagnostic.cordis.yml
new file mode 100644
index 0000000000..c82531c0e9
--- /dev/null
+++ b/examples/acp-agent/subagent-result-diagnostic.cordis.yml
@@ -0,0 +1,17 @@
+# Test-only product-shaped composition: mount a deterministic provider behind
+# the same one-shot tool schema as the public Codex example.
+- id: base
+ name: '@deepseek-ai/cordis-plugin-include'
+ config:
+ path: ./cordis.yml
+ patches:
+ - insert:
+ - id: subagent-result-diagnostic
+ name: './tests/fixtures/subagent-result-diagnostic.ts'
+ - id: tool-subagent-codex
+ name: '@deepseek-ai/dsh-tool-subagent'
+ config:
+ provider: snapshot-diagnostic
+ toolName: subagent_codex
+ backgroundMode: one-shot
+ maxDepth: provider-managed
diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts
index db4a2b5d2f..0ee4fc179c 100644
--- a/examples/acp-agent/tests/acp.snapshot.ts
+++ b/examples/acp-agent/tests/acp.snapshot.ts
@@ -65,6 +65,9 @@ const BACKGROUND_TASK_ADMISSION_CONFIG = fileURLToPath(
)
const PRODUCT_SUBAGENT_CODEX_CONFIG = fileURLToPath(new URL('../product-subagent-codex.cordis.yml', import.meta.url))
const PRODUCT_SUBAGENT_BOTH_CONFIG = fileURLToPath(new URL('../product-subagent-both.cordis.yml', import.meta.url))
+const PRODUCT_SUBAGENT_RESULT_DIAGNOSTIC_CONFIG = fileURLToPath(
+ new URL('../subagent-result-diagnostic.cordis.yml', import.meta.url),
+)
const FS_DIFF_BOUND_CONFIG = fileURLToPath(new URL('./fs-diff-bound.cordis.yml', import.meta.url))
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
@@ -145,7 +148,7 @@ const SCENARIOS: Scenario[] = [
hasModelTurn: true,
recorded: false,
pinsHeader: true,
- headerClass: 'product-subagent-codex',
+ headerClass: 'product-subagent-result-diagnostic',
configPath: PRODUCT_SUBAGENT_CODEX_CONFIG,
},
{
@@ -157,6 +160,17 @@ const SCENARIOS: Scenario[] = [
systemPromptSource: 'product-subagent-codex',
configPath: PRODUCT_SUBAGENT_BOTH_CONFIG,
},
+ {
+ name: 'product-subagent-result-diagnostic',
+ hasModelTurn: true,
+ recorded: false,
+ overridden: true,
+ pinsHeader: true,
+ headerClass: 'product-subagent-codex',
+ systemPromptSource: 'product-subagent-codex',
+ toolSchemasSource: 'product-subagent-codex',
+ configPath: PRODUCT_SUBAGENT_RESULT_DIAGNOSTIC_CONFIG,
+ },
{
name: 'session-title-after-turn',
hasModelTurn: true,
diff --git a/examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts b/examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts
new file mode 100644
index 0000000000..f381bf3a81
--- /dev/null
+++ b/examples/acp-agent/tests/fixtures/subagent-result-diagnostic.ts
@@ -0,0 +1,50 @@
+/** Deterministic provider for model-visible foreground and Job diagnostic snapshots. */
+
+import type { Context } from '@deepseek-ai/cordis'
+import {
+ NO_START_CAPABILITIES,
+ type ResolvedSubagentStartRequest,
+ type SubagentProvider,
+} from '@deepseek-ai/dsh-subagent'
+import { SessionId } from '@deepseek-ai/dsh-session'
+
+export const name = 'subagent-result-diagnostic'
+export const inject = ['subagents']
+
+const DIAGNOSTIC = 'Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt'
+
+class DiagnosticProvider implements SubagentProvider {
+ readonly name = 'snapshot-diagnostic'
+ readonly capabilities = NO_START_CAPABILITIES
+ readonly inheritsParentContext = false
+ private starts = 0
+
+ async start(request: ResolvedSubagentStartRequest) {
+ if (request.signal.aborted) {
+ throw new Error('snapshot diagnostic provider start aborted')
+ }
+ const index = this.starts++
+ if (index > 1) {
+ throw new Error('snapshot diagnostic provider expected exactly two starts')
+ }
+ return {
+ id: SessionId(index === 0
+ ? '00000000-0000-4000-8000-0000000000d1'
+ : '00000000-0000-4000-8000-0000000000d2'),
+ localAgent: undefined,
+ result: Promise.resolve({
+ output: index === 0
+ ? [{ type: 'text' as const, text: 'partial assistant text' }]
+ : [],
+ diagnostic: DIAGNOSTIC,
+ stopReason: 'error' as const,
+ }),
+ dispose: async () => {},
+ }
+ }
+}
+
+/** Register the fixed snapshot provider under the public product provider name. */
+export function apply(ctx: Context): void {
+ ctx.subagents.registerProvider(new DiagnosticProvider())
+}
diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
index 45b62f880f..2e08c0036d 100644
--- a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
+++ b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml
@@ -14,6 +14,8 @@
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
+ config:
+ permissionMode: acceptEdits
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/input.json b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/input.json
new file mode 100644
index 0000000000..b75f1d9580
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/input.json
@@ -0,0 +1,7 @@
+{
+ "steps": [
+ { "op": "initialize" },
+ { "op": "newSession" },
+ { "op": "prompt", "text": "Use subagent_codex in the foreground exactly once; its result will fail with a diagnostic and partial output. Then use subagent_codex in the background exactly once and collect subagent-1 with job_output using wait true. After observing both failures, reply with exactly PARENT_OBSERVED_DIAGNOSTICS. Do not call any other tools." }
+ ]
+}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/replay.override.json b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/replay.override.json
new file mode 100644
index 0000000000..6fbff83b8c
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/replay.override.json
@@ -0,0 +1,42 @@
+[
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "tool-call" },
+ { "type": "tool-call-delta", "index": 0, "id": "call_diagnostic_foreground", "name": "subagent_codex", "argumentsDelta": "{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}" },
+ { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_diagnostic_foreground", "name": "subagent_codex", "arguments": "{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}" } },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
+ { "type": "finish", "reason": { "kind": "tool-calls" } }
+ ]
+ },
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "tool-call" },
+ { "type": "tool-call-delta", "index": 0, "id": "call_diagnostic_background", "name": "subagent_codex", "argumentsDelta": "{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}" },
+ { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_diagnostic_background", "name": "subagent_codex", "arguments": "{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}" } },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
+ { "type": "finish", "reason": { "kind": "tool-calls" } }
+ ]
+ },
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "tool-call" },
+ { "type": "tool-call-delta", "index": 0, "id": "call_diagnostic_output", "name": "job_output", "argumentsDelta": "{\"job_id\":\"subagent-1\",\"wait\":true}" },
+ { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_diagnostic_output", "name": "job_output", "arguments": "{\"job_id\":\"subagent-1\",\"wait\":true}" } },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } },
+ { "type": "finish", "reason": { "kind": "tool-calls" } }
+ ]
+ },
+ {
+ "kind": "chunks",
+ "chunks": [
+ { "type": "block-start", "index": 0, "blockType": "text" },
+ { "type": "text-delta", "index": 0, "text": "PARENT_OBSERVED_DIAGNOSTICS" },
+ { "type": "block-end", "index": 0, "block": { "type": "text", "text": "PARENT_OBSERVED_DIAGNOSTICS" } },
+ { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } },
+ { "type": "finish", "reason": { "kind": "stop" } }
+ ]
+ }
+]
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/session.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/session.jsonl
new file mode 100644
index 0000000000..f1c5ffe374
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/session.jsonl
@@ -0,0 +1,51 @@
+{"type":"session","version":0,"id":"539aa64c-7f37-40ff-abd8-ed45b717be1b","createdAt":1783600629539,"cwd":"{{cwd}}","delegationDepth":0}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498761270,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Use subagent_codex in the foreground exactly once; its result will fail with a diagnostic and partial output. Then use subagent_codex in the background exactly once and collect subagent-1 with job_output using wait true. After observing both failures, reply with exactly PARENT_OBSERVED_DIAGNOSTICS. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"b8004c02-9892-40a7-b7a4-28f04879082c"}]}}
+{"type":"turn/start","seq":1,"time":1785821359466,"data":{"turn":1}}
+{"type":"agent/inbox/spliced","seq":2,"time":1785821359466,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":3,"time":1783600629542,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1785498761313,"data":{"content":[{"type":"text","text":"Use subagent_codex in the foreground exactly once; its result will fail with a diagnostic and partial output. Then use subagent_codex in the background exactly once and collect subagent-1 with job_output using wait true. After observing both failures, reply with exactly PARENT_OBSERVED_DIAGNOSTICS. Do not call any other tools."}],"source":{"kind":"user"},"role":"user","id":"b8004c02-9892-40a7-b7a4-28f04879082c"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785730415287,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"4b8d9730-0b7b-4e14-8a30-3d852f808f0e"},"surfaceOp":"append"}
+{"type":"session/title","seq":6,"time":1785730415287,"data":{"title":"Use subagent_codex in the foreground","messageSeqs":[4],"source":{"kind":"fallback"}}}
+{"type":"request/header","seq":7,"time":1785498761318,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-pro"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
+{"type":"request/context","seq":8,"time":1785730415288,"data":{"provider":"deepseek-official","model":"deepseek-v4-pro"}}
+{"type":"assistant/chunk","seq":9,"time":1783600630822,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":10,"time":1783600630852,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_diagnostic_foreground","name":"subagent_codex","argumentsDelta":"{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}"}}}
+{"type":"assistant/chunk","seq":11,"time":1783600630852,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_diagnostic_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}"}}}}
+{"type":"assistant/chunk","seq":12,"time":1783600630852,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":13,"time":1783600630852,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":14,"time":1786781990608,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_diagnostic_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"92e33995-2f02-4ad5-aec1-9df82cf4d583"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}
+{"type":"tool/call","seq":15,"time":1786781990608,"data":{"turn":1,"step":1,"callId":"call_diagnostic_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe foreground diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":false}"}}
+{"type":"tool/result","seq":16,"time":1786781990613,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_diagnostic_foreground"},"content":[{"type":"tool-result","toolCallId":"call_diagnostic_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt\nPartial output before the run ended:\npartial assistant text"}],"isError":true}],"role":"user","id":"4e84e7b3-40c1-488e-b119-45e8bd7ce448"}},"sourceEventSeqs":[15],"surfaceOp":"append"}
+{"type":"step/end","seq":17,"time":1786781990613,"data":{"turn":1,"step":1}}
+{"type":"step/start","seq":18,"time":1786781990618,"data":{"turn":1,"step":2}}
+{"type":"assistant/chunk","seq":19,"time":1783600630926,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":20,"time":1783600630926,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_diagnostic_background","name":"subagent_codex","argumentsDelta":"{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}"}}}
+{"type":"assistant/chunk","seq":21,"time":1783600630926,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_diagnostic_background","name":"subagent_codex","arguments":"{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}"}}}}
+{"type":"assistant/chunk","seq":22,"time":1783600630944,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":23,"time":1783600630944,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":24,"time":1786781990622,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_diagnostic_background","name":"subagent_codex","arguments":"{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"2fb444e2-7a52-4963-988e-b1ecbc3744d5"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}
+{"type":"tool/call","seq":25,"time":1786781990623,"data":{"turn":1,"step":2,"callId":"call_diagnostic_background","name":"subagent_codex","arguments":"{\"description\":\"Observe background diagnostic\",\"prompt\":\"Return the diagnostic failure.\",\"run_in_background\":true}"}}
+{"type":"agent/inbox/spliced","seq":26,"time":1786781990627,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe background diagnostic) finished [status: failed, error; diagnostic: Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe background diagnostic [status: failed, error; diagnostic: Claude Code unattended decision (mode: dontA…"},"role":"user","id":"de606545-e637-4d9a-ba17-4c722a7331fd"}]}}
+{"type":"tool/result","seq":27,"time":1786781990627,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_diagnostic_background"},"content":[{"type":"tool-result","toolCallId":"call_diagnostic_background","content":[{"type":"text","text":"started background subagent job subagent-1"}],"isError":false}],"role":"user","id":"3377f724-b4a7-4ce1-bed7-774f174917d6"}},"sourceEventSeqs":[25],"surfaceOp":"append"}
+{"type":"step/end","seq":28,"time":1786781990627,"data":{"turn":1,"step":2}}
+{"type":"agent/inbox/spliced","seq":29,"time":1786781990627,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
+{"type":"step/start","seq":30,"time":1786781990632,"data":{"turn":1,"step":3}}
+{"type":"user/message","seq":31,"time":1786781990632,"data":{"content":[{"type":"text","text":"background job subagent-1 (subagent: Observe background diagnostic) finished [status: failed, error; diagnostic: Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe background diagnostic [status: failed, error; diagnostic: Claude Code unattended decision (mode: dontA…"},"role":"user","id":"de606545-e637-4d9a-ba17-4c722a7331fd"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":32,"time":1783600631009,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":33,"time":1783600631009,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"call_diagnostic_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}
+{"type":"assistant/chunk","seq":34,"time":1783600631009,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_diagnostic_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}}}
+{"type":"assistant/chunk","seq":35,"time":1785498761338,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":36,"time":1785730415297,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/message","seq":37,"time":1785730415298,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_diagnostic_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"f43f988b-bc08-4811-8671-8edc0613f0d0"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
+{"type":"tool/call","seq":38,"time":1786781990636,"data":{"turn":1,"step":3,"callId":"call_diagnostic_output","name":"job_output","arguments":"{\"job_id\":\"subagent-1\",\"wait\":true}"}}
+{"type":"tool/result","seq":39,"time":1786781990640,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"call_diagnostic_output"},"content":[{"type":"tool-result","toolCallId":"call_diagnostic_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Claude Code unattended decision (mode: dontAsk; request: tool permission; decision: denied): Claude Code denied the request before an interactive prompt]"}],"isError":false}],"role":"user","id":"6785120f-ae46-48d0-9f3f-d6cd1e6fc5d7"}},"sourceEventSeqs":[38],"surfaceOp":"append"}
+{"type":"step/end","seq":40,"time":1786781990640,"data":{"turn":1,"step":3}}
+{"type":"step/start","seq":41,"time":1786781990645,"data":{"turn":1,"step":4}}
+{"type":"assistant/chunk","seq":42,"time":1786781990649,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":43,"time":1786781990649,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"PARENT_OBSERVED_DIAGNOSTICS"}}}
+{"type":"assistant/chunk","seq":44,"time":1786781990649,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_OBSERVED_DIAGNOSTICS"}}}}
+{"type":"assistant/chunk","seq":45,"time":1786781990649,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
+{"type":"assistant/chunk","seq":46,"time":1786781990649,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":47,"time":1786781990649,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_OBSERVED_DIAGNOSTICS"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"49b868e8-2608-47e0-aaf8-b308ffe8194d"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[42,43,44,45,46],"surfaceOp":"append"}
+{"type":"step/end","seq":48,"time":1786781990650,"data":{"turn":1,"step":4}}
+{"type":"turn/end","seq":49,"time":1786781990650,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/stdout.expected.jsonl
new file mode 100644
index 0000000000..83e4ef4368
--- /dev/null
+++ b/examples/acp-agent/tests/snapshots/product-subagent-result-diagnostic/stdout.expected.jsonl
@@ -0,0 +1,4 @@
+{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
+{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
+{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PARENT_OBSERVED_DIAGNOSTICS"}}}}
+{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/knip.json b/knip.json
index 3017292382..8f4b21a97f 100644
--- a/knip.json
+++ b/knip.json
@@ -52,6 +52,7 @@
"acp-agent/tests/fixtures/parent-sandbox-override.ts",
"acp-agent/tests/fixtures/partial-landlock-sandbox.ts",
"acp-agent/tests/fixtures/subagent-durability-failure.ts",
+ "acp-agent/tests/fixtures/subagent-result-diagnostic.ts",
"acp-agent/tests/fixtures/subagent-report-fence.ts",
"acp-agent/tests/fixtures/subagent-settlement-marker.ts",
"acp-agent/tests/fixtures/workspace-context-compaction.ts",
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index 5a812806da..c312f5d30e 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -4127,7 +4127,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentResult',
- declaration: 'export interface SubagentResult {\n readonly output: ContentBlock[];\n readonly structured?: unknown;\n readonly stopReason: SubagentStopReason;\n}',
+ declaration: 'export interface SubagentResult {\n readonly output: ContentBlock[];\n readonly structured?: unknown;\n readonly diagnostic?: string;\n readonly stopReason: SubagentStopReason;\n}',
},
{
name: 'SubagentRun',
diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml
index a165540575..f7286cee7d 100644
--- a/packages/subagent/subagent-claude-code/README.i18n.yaml
+++ b/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: 1a0d6e32b8610769dcc5d8342a4fe88d0c884085
-README.zh.md: 78dab14e5eaddc06ccd07b69dc952a09380e0428
+README.md: c74c092d58d7853cedee3c5f326467a5036c50fb
+README.zh.md: e87f03399e6ffd926d0ea25c6f84339ba8c94d6f
diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md
index 1a0d6e32b8..c74c092d58 100644
--- a/packages/subagent/subagent-claude-code/README.md
+++ b/packages/subagent/subagent-claude-code/README.md
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
-This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
+This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns either the strict final answer or safe failure detail through the shared [`dsh-subagent`](../subagent/README.md) result contract.
## Start and ownership
@@ -14,9 +14,9 @@ Local cancellation wins the result race and maps to `aborted`. `dispose()` is id
## Native settings and interaction
-The provider deliberately omits the SDK `settingSources` option. The official SDK therefore reads the host's normal user, project, and local Claude settings relative to the parent Session cwd, including native account state and product configuration. The provider neither copies nor filters those files and does not create or modify login state.
+The provider deliberately omits the SDK `settingSources` option. The official SDK therefore reads the host's normal user, project, and local Claude settings relative to the parent Session cwd, including native account state and product configuration. The provider neither copies nor filters those files and does not create or modify login state. The Profile-selected `permissionMode` is the one query-level override: Claude Code still owns its settings and sandbox, while the selected native mode decides how this unattended query handles permission checks.
-Each query sets `persistSession: false` and disables `AskUserQuestion`. It supplies no `canUseTool`, elicitation, or dialog callback, so unattended interactions fail through the SDK instead of waiting for a user interface this provider does not own.
+Each query sets `persistSession: false` and disables `AskUserQuestion`. Except in bypass mode, `canUseTool` immediately denies requests that still require human approval. MCP elicitation is declined, the known refusal fallback dialog is cancelled, and undeclared dialog kinds use the SDK's no-dialog failure behavior. These decisions never wait for a user interface. A permission denial or unattended callback that contributes to a failed run produces an optional `SubagentResult.diagnostic` containing only the product, effective mode, request category, decision, and fixed safe reason; the shared result boundary limits the complete text to 4096 UTF-8 bytes. Successful and locally cancelled runs do not expose the captured failure detail.
## Capabilities and context
@@ -27,8 +27,17 @@ The provider advertises no optional start-time capabilities and reports `inherit
| Key | Default | Meaning |
|---|---|---|
| `env` | `{}` | Explicit SDK/CLI environment layered over the shared credential-scrubbed parent environment. |
+| `permissionMode` | `dontAsk` | Native non-interactive permission policy fixed for every run from this Provider instance. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
+| `permissionMode` value | Native behavior |
+|---|---|
+| `dontAsk` | Deny operations that are not already authorized instead of prompting. |
+| `acceptEdits` | Accept file edits; any remaining permission prompt is denied by the unattended callback. |
+| `auto` | Let Claude Code's native classifier allow or deny permission requests. |
+| `plan` | Run Claude Code in its native planning-only mode without tool execution. |
+| `bypassPermissions` | Explicitly set the SDK's dangerous confirmation and bypass permission checks. |
+
Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-claude-code` and mount it once on the host plane; loading the provider starts no Claude process until a tool call. Full Agent Presets carry a matching product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
@@ -39,6 +48,7 @@ The standalone composition below shows the complete explicit capability. A Profi
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
config:
+ permissionMode: acceptEdits
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
@@ -69,7 +79,7 @@ The project owner's identity-scoped distribution authorization covers the offici
#### What the model sees
-The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd, while its model, system instructions, tools, permissions, and authentication come from the host's native Claude settings and product installation.
+The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from the host's native Claude settings and product installation, while the Provider's Profile configuration fixes the query's non-interactive permission mode.
#### Token effect
@@ -83,7 +93,7 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own
#### What the model sees
-Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer and status through `job_output`, and let `job_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
+Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or an error containing the stop reason and optional safe diagnostic for a non-completed result. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the final answer or failed status detail through `job_output`, and let `job_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, product ids, tool inputs, and raw protocol payloads are not copied into the parent Session.
#### Token effect
@@ -99,7 +109,7 @@ Append-only: foreground adds one result after the reusable parent prefix, while
- **Host settings are intentionally authoritative** — project and user settings can change model, tools, and behavior; the provider does not provide a filtered or hermetic production mode.
- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
-- **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending.
-- **Product payload is final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Job ids, notices, and status come from the shared job runtime.
+- **No human interaction path** — `AskUserQuestion` is disabled, permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of suspending.
+- **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local, while generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
- **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored.
diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md
index 78dab14e5e..e87f03399e 100644
--- a/packages/subagent/subagent-claude-code/README.zh.md
+++ b/packages/subagent/subagent-claude-code/README.zh.md
@@ -2,7 +2,7 @@
[English](README.md) | 中文
-本包(package)注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
+本包(package)注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或安全的失败说明。
## 启动与所有权
@@ -14,9 +14,9 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
## 原生设置与交互
-提供方故意省略 SDK 的 `settingSources` 选项。因此,官方 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置,包括原生账户状态与产品配置。提供方既不复制也不过滤这些文件,也不会创建或修改登录状态。
+提供方故意省略 SDK 的 `settingSources` 选项。因此,官方 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置,包括原生账户状态与产品配置。提供方既不复制也不过滤这些文件,也不会创建或修改登录状态。Profile 选择的 `permissionMode` 是唯一的 query 级覆盖:Claude Code 仍拥有其设置与沙箱,而所选原生模式决定这个无人值守 query 如何处理权限检查。
-每次 query 都设置 `persistSession: false` 并禁用 `AskUserQuestion`。提供方不设置 `canUseTool`、elicitation 或对话回调,因此无人值守交互会经 SDK 失败,而不会等待本提供方不负责的用户界面。
+每次 query 都设置 `persistSession: false` 并禁用 `AskUserQuestion`。除 bypass 模式外,`canUseTool` 会立即拒绝仍需人工审批的请求。MCP elicitation 会被拒绝,已知的拒绝回退对话会被取消,未声明的对话类型则使用 SDK 的无对话失败行为。这些决定都不会等待用户界面。若权限拒绝或无人值守回调参与了一次失败运行,提供方会生成可选的 `SubagentResult.diagnostic`,其中只包含产品、有效模式、请求类别、决定与固定的安全原因;共享结果边界会把完整文本限制在 4096 个 UTF-8 字节以内。成功运行与本地取消不会公开已捕获的失败说明。
## 能力与上下文
@@ -27,8 +27,17 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
| 配置键 | 默认值 | 含义 |
|---|---|---|
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
+| `permissionMode` | `dontAsk` | 为该提供方实例的每次运行固定原生非交互权限策略。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
+| `permissionMode` 值 | 原生行为 |
+|---|---|
+| `dontAsk` | 不弹出提示,直接拒绝尚未获授权的操作。 |
+| `acceptEdits` | 接受文件编辑;其余权限提示由无人值守回调拒绝。 |
+| `auto` | 由 Claude Code 原生分类器允许或拒绝权限请求。 |
+| `plan` | 使用 Claude Code 原生的仅规划模式,不执行工具。 |
+| `bypassPermissions` | 显式设置 SDK 的危险确认并跳过权限检查。 |
+
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-claude-code`,并在 host plane(宿主平面)挂载一次;加载提供方本身不会在工具调用前启动 Claude 进程。完整 Agent Preset 携带对应的产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
@@ -39,6 +48,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
config:
+ permissionMode: acceptEdits
env:
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
@@ -69,7 +79,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
#### 模型看到的内容
-Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、权限和身份验证来自宿主机原生 Claude 设置与产品安装。
+Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自宿主机原生 Claude 设置与产品安装,而提供方的 Profile 配置会固定该 query 的非交互权限模式。
#### 对 token 的影响
@@ -83,7 +93,7 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
#### 模型看到的内容
-通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开最终答案与状态,并允许 `job_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
+通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案;若结果未完成,错误中会包含终止原因和可选的安全诊断。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开最终答案或失败状态 detail,并允许 `job_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息、产品标识符、工具输入和原始协议载荷均不会复制到父会话。
#### 对 token 的影响
@@ -99,7 +109,7 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
- **宿主设置有意保持权威**:项目和用户设置可以改变模型、工具与行为;本提供方不提供经过筛选或与宿主环境隔离的生产模式。
- **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
-- **没有人工交互路径**:`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。
-- **产品载荷仅包含最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Job id、通知与状态来自共享作业运行时。
+- **没有人工交互路径**:`AskUserQuestion` 被禁用,权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败而不会挂起。
+- **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部,通用 Job id、通知与状态来自共享作业运行时。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
- **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。
diff --git a/packages/subagent/subagent-claude-code/src/index.ts b/packages/subagent/subagent-claude-code/src/index.ts
index ccd150b746..3894369fc6 100644
--- a/packages/subagent/subagent-claude-code/src/index.ts
+++ b/packages/subagent/subagent-claude-code/src/index.ts
@@ -18,8 +18,11 @@ import {
type SubagentProvider,
} from '@deepseek-ai/dsh-subagent'
import {
+ CLAUDE_CODE_PERMISSION_MODES,
+ DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
DEFAULT_DISPOSE_GRACE_MS,
startClaudeCodeRun,
+ type ClaudeCodePermissionMode,
type ClaudeCodeRunSpec,
} from './run.ts'
@@ -28,19 +31,23 @@ export const inject = ['subagents', 'subprocess']
/* jscpd:ignore-start -- sibling product providers intentionally expose the
* same two deployment-owned fields without adding a shared config owner. */
-/** Deployment-owned environment and process-release bound. */
+/** Deployment-owned permission, environment, and process-release settings. */
export interface Config {
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
*/
env?: Record
+ /** Native non-interactive permission mode fixed for this Provider instance. */
+ permissionMode?: ClaudeCodePermissionMode
/** Grace in milliseconds for Claude Code process-tree termination. */
disposeGraceMs?: number
}
export const Config: z = z.object({
env: z.dict(z.string()).default({}),
+ permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES])
+ .default(DEFAULT_CLAUDE_CODE_PERMISSION_MODE),
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
})
@@ -78,6 +85,7 @@ class ClaudeCodeProvider implements SubagentProvider {
parentCwd,
),
executable,
+ permissionMode: this.config.permissionMode,
env: this.config.env,
disposeGraceMs: this.config.disposeGraceMs,
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),
@@ -94,10 +102,14 @@ class ClaudeCodeProvider implements SubagentProvider {
/**
* Register the fixed `claude-code` provider.
* @param ctx - context carrying shared subagent and subprocess services.
- * @param config - explicit child environment and disposal grace.
+ * @param config - permission mode, child environment, and disposal grace.
*/
export function apply(ctx: Context, config: Config): void {
- const resolved = config as ResolvedConfig
+ const resolved: ResolvedConfig = {
+ env: config.env as Record,
+ permissionMode: config.permissionMode ?? DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
+ disposeGraceMs: config.disposeGraceMs as number,
+ }
assertPositiveFinite(
'subagent-claude-code',
'disposeGraceMs',
diff --git a/packages/subagent/subagent-claude-code/src/run.ts b/packages/subagent/subagent-claude-code/src/run.ts
index 6c1e0a8dbf..ffcac2bedf 100644
--- a/packages/subagent/subagent-claude-code/src/run.ts
+++ b/packages/subagent/subagent-claude-code/src/run.ts
@@ -38,6 +38,37 @@ import {
/** Default POSIX grace between subprocess termination tiers. */
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
+/** Profile-selectable non-interactive Claude Code permission mode. */
+export type ClaudeCodePermissionMode =
+ | 'dontAsk'
+ | 'acceptEdits'
+ | 'auto'
+ | 'plan'
+ | 'bypassPermissions'
+
+/** Claude Code permission modes that cannot wait for a human response. */
+export const CLAUDE_CODE_PERMISSION_MODES = [
+ 'dontAsk',
+ 'acceptEdits',
+ 'auto',
+ 'plan',
+ 'bypassPermissions',
+] as const satisfies readonly ClaudeCodePermissionMode[]
+
+/** Safe default for unattended Claude Code runs. */
+export const DEFAULT_CLAUDE_CODE_PERMISSION_MODE: ClaudeCodePermissionMode = 'dontAsk'
+
+const SUPPORTED_UNATTENDED_DIALOG_KINDS = ['refusal_fallback_prompt']
+
+function unattendedDiagnostic(
+ mode: ClaudeCodePermissionMode,
+ request: 'tool permission' | 'MCP elicitation' | 'user dialog',
+ decision: 'denied' | 'declined' | 'cancelled',
+ reason: string,
+): string {
+ return `Claude Code unattended decision (mode: ${mode}; request: ${request}; decision: ${decision}): ${reason}`
+}
+
/* jscpd:ignore-start -- sibling providers intentionally keep product-private
* run inputs and error normalization instead of adding a shared lifecycle owner. */
/** Fully resolved inputs for one official Claude Agent SDK query. */
@@ -46,6 +77,8 @@ export interface ClaudeCodeRunSpec {
readonly cwd: string
/** Exact native Claude Code executable resolved from the host PATH. */
readonly executable: string
+ /** Profile-selected native non-interactive permission mode. */
+ readonly permissionMode: ClaudeCodePermissionMode
/** Explicit deployment/test environment layered after shared scrubbing. */
readonly env: Record