From b2178ade8028597e7565db77dfe3ef4ecd68b99d Mon Sep 17 00:00:00 2001 From: pku-xht Date: Sat, 15 Aug 2026 03:25:43 +0800 Subject: [PATCH] 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') - }) })