mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(subagent): support named Claude Code provider instances
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: 452ff1cca7e4e5f91f8c35092761ebe83f3ff174
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: a62bf6faa3c9bba5326da1de20ecbc2946c02bcc
|
||||
2026-08-10-product-subagent-providers-in-shared-host.md: 2798431709307e50a1ee16c7fc595bcead223f59
|
||||
2026-08-10-product-subagent-providers-in-shared-host.zh.md: 981b1e2cd305c1410dcd744e3aea5028eb283806
|
||||
|
||||
+6
-6
@@ -6,23 +6,23 @@ English | [中文](2026-08-10-product-subagent-providers-in-shared-host.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) were first shipped as independently installable packages that a deployment loaded beside the common subagent tool. Agent Presets later became the ordinary owner of one agent's model-visible tools, but a preset cannot safely own these product providers: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
|
||||
The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) were first shipped as independently installable packages that a deployment loaded beside the common subagent tool. Agent Presets later became the ordinary owner of one agent's model-visible tools, but a preset cannot safely own these product providers: `ctx.subagents` is a process registry, provider names are unique within the Host, and host consumers resolve the same registry across sessions. Repeated preset composition would therefore contend for the same configured names. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
|
||||
|
||||
The placement decision must preserve two independent facts. Loading a provider must not start or authenticate a product, while enabling a tool must remain per preset so two sessions can expose different products. A global product switch, a provider instance per agent, or pre-enumerated combination presets would each create a second owner for one of those facts.
|
||||
|
||||
## Decision
|
||||
|
||||
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider package and mounts it once on the host plane. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
|
||||
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider package and mounts the required instances on the host plane. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns each row's registry identity: Claude Code accepts multiple unique `providerName` values while preserving `claude-code` as its default; Codex still registers only its `codex` default. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows whose `provider` and `toolName` values expose exactly the configured instances needed by one agent without changing the Host registry.
|
||||
|
||||
This note continues to own why a mounted product provider belongs on the host plane while its model-facing tool belongs to an Agent Preset. The production-install exclusion decision owns which Profiles install those optional packages. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
|
||||
|
||||
The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, or test authentication. It may supply each mounted Provider's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing commands and product failures remain local to the attempted delegation.
|
||||
The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, or test authentication. It may supply each mounted Provider instance's deployment configuration, including the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing commands and product failures remain local to the attempted delegation.
|
||||
|
||||
Only a Profile that selects the Claude Code provider carries the Claude Agent SDK's optional platform CLI payload. Production still resolves the host `claude`; the SDK payload remains provider-package installation cost rather than the production executable.
|
||||
|
||||
## Verification
|
||||
|
||||
The base bundle test proves production `dsh-base` contains neither product provider dependency nor provider row. The Web composition explicitly mounts both optional providers and covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Package-owned Loader compositions prove the Codex-only and dual-provider opt-in paths register the selected providers without starting a product process. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
|
||||
The base bundle test proves production `dsh-base` contains neither product provider dependency nor provider row. The Web composition explicitly mounts both optional providers and covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Package-owned Loader compositions prove the Codex-only path and a Host containing the default Codex instance plus two named Claude instances register without starting a product process. Keyless ACP snapshots pin the model-visible tool schemas for one product and for independently named product tools, while provider tests separately prove native executable resolution, configuration isolation, failure, cancellation, and process-tree quiescence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,12 +30,12 @@ The base bundle test proves production `dsh-base` contains neither product provi
|
||||
|
||||
**Store global or per-Profile product enable switches.** A process switch competes with the Preset as owner of model-visible tools and cannot express two sessions using different combinations. Availability and authentication are deployment facts, not another persisted product state.
|
||||
|
||||
**Mount a provider inside every Agent Preset.** Provider names belong to a process registry, so the second session would collide with the first. Host consumers also need the registry independently of any one agent's lifetime.
|
||||
**Mount providers inside every Agent Preset.** Provider names belong to a process registry, so repeated session composition would collide on the same configured names. Host consumers also need the registry independently of any one agent's lifetime.
|
||||
|
||||
**Ship four product-combination presets.** Four identities duplicate complete compositions to represent two independent tool rows. Ordinary rows already express the full matrix without adding roster or maintenance state.
|
||||
|
||||
## Consequences
|
||||
|
||||
A user installs each selected product provider in a Profile and exposes its tool through the same Agent Preset authoring path as other plugins. Each new session receives exactly the tools its chosen preset contributes. Profiles that do not select a product provider carry no corresponding package or module-loading footprint; loading a selected provider still starts no product process, login, model call, or product home.
|
||||
A user installs each selected product provider in a Profile, mounts the required named instances, and exposes their tools through the same Agent Preset authoring path as other plugins. Each new session receives exactly the tools its chosen preset contributes. Profiles that do not select a product provider carry no corresponding package or module-loading footprint; loading selected instances still starts no product process, login, model call, or product home.
|
||||
|
||||
The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is a two-layer opt-in: the Profile owns installation and host-plane registration, while the Preset owns per-agent exposure. Selecting the Claude provider also accepts its current SDK optional-payload installation cost.
|
||||
|
||||
+7
-7
@@ -6,36 +6,36 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。如果要求用户同时编辑 Profile 和 Preset,也会使通用 preset 行本身不完整。
|
||||
[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent(智能体)的模型可见工具的常规责任方,但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称在 Host 内唯一,而宿主消费方会跨会话解析同一个注册表。因此,重复组装 preset 会争用同一组已配置名称。如果要求用户同时编辑 Profile 和 Preset,也会使通用 preset 配置项本身不完整。
|
||||
|
||||
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具是否启用仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset,都会为其中一个事实另设第二责任方。
|
||||
|
||||
## 决策
|
||||
|
||||
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方包,并在 host plane 挂载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex` 与 `subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
|
||||
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方包,并在 host plane 挂载所需实例。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.md)负责每个配置项的注册身份:Claude Code 接受多个唯一的 `providerName`,同时保留 `claude-code` 作为默认值;Codex 仍只注册默认的 `codex`。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 通过普通 `dsh-tool-subagent` 配置项的 `provider` 与 `toolName` 准确公开单个 agent 所需的已配置实例,而无需更改 Host 注册表。
|
||||
|
||||
本说明继续负责解释为什么已经挂载的产品提供方属于 host plane,而面向模型的工具属于 Agent Preset。生产安装排除决策负责哪些 Profile 安装这些可选包。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
|
||||
|
||||
这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本或测试身份验证。它可以提供每个已挂载 Provider 的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。命令缺失和产品故障仍局限于发生问题的那次委派。
|
||||
这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本或测试身份验证。它可以提供每个已挂载 Provider 实例的部署配置,包括由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。命令缺失和产品故障仍局限于发生问题的那次委派。
|
||||
|
||||
只有选择 Claude Code 提供方的 Profile 才会携带 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷。生产环境仍解析宿主提供的 `claude`;这份 SDK 载荷是提供方包的安装成本,而不是生产可执行文件。
|
||||
|
||||
## 验证
|
||||
|
||||
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置行。Web 组装显式挂载两个可选提供方,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明 Codex-only 与双提供方按需启用路径会注册选中的提供方,而不会启动产品进程。无密钥 ACP(Agent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
|
||||
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置项。Web 组装显式挂载两个可选提供方,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明 Codex-only 路径以及包含默认 Codex 实例与两个命名 Claude 实例的 Host 会完成注册,而不会启动产品进程。无密钥 ACP(Agent Client Protocol)快照固定单个产品与独立命名产品工具的模型可见 schema,提供方测试则另行证明原生可执行文件解析、配置隔离、失败、取消和进程树完全停稳。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但要求用户同时编辑 Profile 与 Preset。生产安装排除决策接受这项安装取舍;本说明保留的要求是,任何被选中的提供方都在 host plane 挂载一次,而不是放入 preset。
|
||||
**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但要求用户同时编辑 Profile 与 Preset。生产安装排除决策接受这项安装取舍;本说明保留的要求是,任何被选中的提供方实例都在 host plane 挂载,而不是放入 preset。
|
||||
|
||||
**存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
|
||||
|
||||
**在每个 Agent Preset 内挂载一个提供方。** 提供方名称属于进程级注册表,因此第二个会话会与第一个冲突。宿主消费方也需要独立于任何单个 agent 的生命周期使用该注册表。
|
||||
**在每个 Agent Preset 内挂载提供方。** 提供方名称属于进程级注册表,因此重复组装会话会在同一组已配置名称上发生冲突。宿主消费方也需要独立于任何单个 agent 的生命周期使用该注册表。
|
||||
|
||||
**交付四个产品组合 preset。** 四个身份会复制完整组装,只为表示两条独立的工具行。普通行已经能表达完整矩阵,无需新增名单或维护状态。
|
||||
|
||||
## 后果
|
||||
|
||||
用户在 Profile 中安装每个被选中的产品提供方,再通过与其他插件相同的 Agent Preset 创作路径暴露它的工具。每个新会话只会获得其所选 preset 所贡献的工具。没有选择产品提供方的 Profile 不承担对应包或模块的加载开销;加载已选择的提供方仍不会启动产品进程、登录、调用模型或创建产品主目录。
|
||||
用户在 Profile 中安装每个被选中的产品提供方,挂载所需命名实例,再通过与其他插件相同的 Agent Preset 创作路径公开这些实例的工具。每个新会话只会获得其所选 preset 所贡献的工具。没有选择产品提供方的 Profile 不承担对应包或模块的加载开销;加载已选择的实例仍不会启动产品进程、登录、调用模型或创建产品主目录。
|
||||
|
||||
宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是两层按需启用:Profile 负责安装与 host plane 注册,Preset 负责按 agent 暴露。选择 Claude 提供方还会接受当前 SDK 可选载荷的安装成本。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: f65c0626ad22db8f3e7d2a543c7aa87e58df54d4
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 97ac527b8e89cc07d65aa28102ba43d648b1b64c
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.md: fb672f5c326ad240964e1e6c051a4f18d8d1552e
|
||||
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 11f5c8f1c47be9e80386832efbe0b8b5675b3437
|
||||
|
||||
+7
-7
@@ -12,12 +12,12 @@ The product integrations must not become second owners for task text, cwd, cance
|
||||
|
||||
## Decision
|
||||
|
||||
The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and diagnostic production. 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 whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity and static tool binding, the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice, and the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and diagnostic production. Claude Code accepts multiple named instances; Codex still registers its single default name. 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.
|
||||
|
||||
```text
|
||||
fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
|
||||
configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
|
||||
foreground <- final product outcome
|
||||
background -> ctx.jobs / dsh-tool-jobs -> Job id / state / notice / controls
|
||||
both -> provider disposal -> dsh-subprocess -> whole-tree exit
|
||||
@@ -48,9 +48,9 @@ Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-comp
|
||||
|
||||
## Claude Code provider
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers the fixed `claude-code` provider and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
`@deepseek-ai/dsh-subagent-claude-code` registers a Profile-selected provider name that defaults to `claude-code` and invokes `@anthropic-ai/claude-agent-sdk@0.3.220`. Before each run, the provider resolves the fixed `claude` executable name through the host subprocess execution world and passes that exact path as `pathToClaudeCodeExecutable`; the SDK therefore uses the native product that launched DSH rather than selecting its platform `optionalDependency`. A Windows `.cmd` or `.bat` path crosses `cmd.exe /v:off` as a quoted per-spawn environment expansion, so percent, ampersand, and exclamation path components remain data without changing the shared subprocess contract. The provider uses the official `query()` entrypoint and passes the SDK's `spawnClaudeCodeProcess` arguments, cwd, environment, and forwarded signal to `dsh-subprocess`; its private `SpawnedProcess` adapter exposes only the stream, event, kill, and exit facts the SDK requires.
|
||||
|
||||
The public configuration contains an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
|
||||
The public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a five-value native `permissionMode` that defaults to `dontAsk`. Each named instance retains those resolved values for its own runs. Each run creates its own `AbortController`, sets `persistSession: false`, disables `AskUserQuestion`, and passes the resolved mode to the SDK; only `bypassPermissions` receives the SDK's explicit dangerous confirmation. The provider deliberately omits `settingSources`, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.
|
||||
|
||||
The provider publishes only after both the SDK `Query` and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a `result` message has `subtype: "success"`, `is_error: false`, and a nonblank `result`, and the iterator then ends normally. Every SDK error subtype, an error-marked success, a missing result, iterator failure, protocol failure, or process failure becomes `error`. When a permission denial or unattended callback contributes to that failure, the result may additionally carry the bounded, non-assistant diagnostic owned by the non-interactive permissions decision. SDK turn, budget, and structured-output limits are not token-window facts, and the SDK exposes no native refusal terminal, so this provider produces neither `max-tokens` nor `refusal`. Local cancellation wins and becomes `aborted` without permission detail.
|
||||
|
||||
@@ -60,7 +60,7 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
|
||||
|
||||
## Distribution and evidence
|
||||
|
||||
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.
|
||||
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 the default Codex instance and two named Claude Code instances expose independent one-shot tools 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, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended command rejection with safe diagnostic and no file side effect, explicit dangerous-bypass writing in suite-owned temporary storage, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
|
||||
|
||||
@@ -78,7 +78,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
|
||||
|
||||
**A shared product-process helper package.** The existing subagent and subprocess seams already own every shared task, result, environment, and process-tree concern. A new helper would duplicate ownership without deleting either private product adapter, so each adapter calls the existing seams directly.
|
||||
|
||||
**A model-visible product selector.** Product availability and authentication are deployment facts. Two fixed tools keep each schema and provider binding explicit and avoid adding dynamic selection state to the common service.
|
||||
**A model-visible product selector.** Product availability, instance configuration, and authentication are deployment facts. Profile-bound tools keep each schema and provider binding explicit and avoid adding dynamic selection state to the common service.
|
||||
|
||||
**Product doubles as required evidence.** Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.
|
||||
|
||||
@@ -88,7 +88,7 @@ The project owner's distribution authorization is scoped to the official `@anthr
|
||||
|
||||
## Consequences
|
||||
|
||||
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.
|
||||
Users delegate through Profile-configured one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); named instance identity and tool binding are owned by the [named-instance decision](2026-08-18-product-subagent-named-instances.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
|
||||
|
||||
Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, workspace settings, and selected Provider mode. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
|
||||
|
||||
|
||||
+7
-7
@@ -12,12 +12,12 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责各产品提供方的 Profile 模式选择与诊断生产。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
|
||||
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.md)负责 Profile 选择的提供方身份与静态工具绑定,[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责显式 Profile 安装与 host plane(宿主平面)放置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择,[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.md)则负责各产品提供方的 Profile 模式选择与诊断生产。Claude Code 接受多个命名实例;Codex 仍只注册单个默认名称。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。
|
||||
|
||||
这两个提供方都报告 `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` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
|
||||
|
||||
```text
|
||||
fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
|
||||
configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
|
||||
foreground <- final product outcome
|
||||
background -> ctx.jobs / dsh-tool-jobs -> Job id / state / notice / controls
|
||||
both -> provider disposal -> dsh-subprocess -> whole-tree exit
|
||||
@@ -48,9 +48,9 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
|
||||
|
||||
## Claude Code 提供方
|
||||
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册固定的 `claude-code` 提供方,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定名称 `claude`,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
`@deepseek-ai/dsh-subagent-claude-code` 注册由 Profile 选择、默认值为 `claude-code` 的提供方名称,并调用 `@anthropic-ai/claude-agent-sdk@0.3.220`。每次运行前,提供方经宿主 subprocess 执行世界解析固定的 `claude` 可执行文件名称,并把准确路径作为 `pathToClaudeCodeExecutable` 交给 SDK;SDK 因此使用启动 DSH 的原生产品,而不是选择自身的 platform `optionalDependency`。Windows `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境展开值穿过 `cmd.exe /v:off`,因此路径中的百分号、与号和感叹号仍只是数据,且无需改变共享子进程约定。提供方使用官方 `query()` 入口点,并将 SDK 的 `spawnClaudeCodeProcess` 参数、cwd、环境和转发的信号交给 `dsh-subprocess`;其私有 `SpawnedProcess` 适配器只公开 SDK 所需的流、事件、终止和退出事实。
|
||||
|
||||
公开配置包含显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
|
||||
公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS` 的 `disposeGraceMs`,以及默认使用 `dontAsk` 的五值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。每次运行都会创建自己的 `AbortController`,设置 `persistSession: false`、禁用 `AskUserQuestion`,并把已解析模式传给 SDK;只有 `bypassPermissions` 会取得 SDK 的显式危险确认。提供方故意省略 `settingSources`,因此 SDK 会相对于父会话 cwd 读取宿主机常规的用户、项目和本地 Claude 设置。它既不复制也不过滤这些设置,也不会创建或修改登录状态。其余权限提示会被拒绝,MCP elicitation 会被拒绝,阻塞对话会快速失败,而不会等待本提供方不负责的用户界面。
|
||||
|
||||
只有在 SDK `Query` 与受管的活动 CLI 句柄都已存在后,提供方才会发布运行。它会消费完整的 SDK 流;只有 `result` 消息具有 `subtype: "success"`、`is_error: false` 和非空白 `result`,且迭代器随后正常结束时,运行才会完成。所有 SDK 错误子类型、标记为错误的成功消息、结果缺失、迭代器失败、协议失败或进程失败都会成为 `error`。当权限拒绝或无人值守回调参与了该失败时,结果还可以携带由非交互权限决策负责的有界、非 assistant 诊断。SDK 的轮次、预算和结构化输出限制不表示 token 窗口耗尽,而且 SDK 没有原生的拒绝终止状态,因此本提供方不会产生 `max-tokens` 或 `refusal`。本地取消会胜出并成为 `aborted`,且不附带权限说明。
|
||||
|
||||
@@ -60,7 +60,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
|
||||
|
||||
## 分发与证据
|
||||
|
||||
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示的显式 Profile 配置,在同一个上下文中验证两个固定一次性工具会与通用 Job 控制工具一起公开可选后台调度,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
|
||||
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示的显式 Profile 配置,验证默认 Codex 实例与两个命名 Claude Code 实例会和通用 Job 控制工具一起公开彼此独立的一次性工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
|
||||
|
||||
Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、带安全诊断且不产生文件副作用的无人值守命令拒绝、测试拥有临时存储中的显式危险绕过写入、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。
|
||||
|
||||
@@ -78,7 +78,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
|
||||
|
||||
**共享产品进程辅助包。** 现有 subagent 与子进程 seam 已负责围绕任务、结果、环境和进程树的全部共享职责。新辅助包无法删除任一私有产品适配器,只会造成责任重复,因此每个适配器都会直接调用现有 seam。
|
||||
|
||||
**面向模型的产品选择器。** 产品可用性和身份验证属于部署事实。两个固定工具使各自的 schema 与提供方绑定保持明确,也避免在通用服务中添加动态选择状态。
|
||||
**面向模型的产品选择器。** 产品可用性、实例配置和身份验证属于部署事实。由 Profile 绑定的工具使各自的 schema 与提供方绑定保持明确,也避免在通用服务中添加动态选择状态。
|
||||
|
||||
**以产品替身作为强制证据。** 替身可以穷尽覆盖私有协议分支,但无法证明包导出、官方发行版、身份验证或真实进程行为。强制证据会驱动每个官方产品连接回环模型 fixture。
|
||||
|
||||
@@ -88,7 +88,7 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl
|
||||
|
||||
## 后果
|
||||
|
||||
用户通过官方产品集成支持的两个稳定一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
|
||||
用户通过由 Profile 配置、并由官方产品集成支持的一次性工具进行委派。显式 Profile 安装与 host plane 提供方放置由[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)负责;命名实例身份与工具绑定由[命名实例决策](2026-08-18-product-subagent-named-instances.md)负责;按 Preset 暴露工具以及默认前台且可选通用 Job 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占作业结算与进程树完全停稳的责任。
|
||||
|
||||
每次委派都要承担新建产品进程和独立模型上下文的开销。成功的产品载荷仍只有最终 assistant 文本;失败的产品运行可以另行公开共享安全诊断。后台调度还会额外公开通用 Job id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态、工作区设置和所选提供方模式。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-product-subagent-named-instances.md
|
||||
2026-08-18-product-subagent-named-instances.md: 6b069727ebba7ebcf34444f9ebdb287c00bf315d
|
||||
2026-08-18-product-subagent-named-instances.zh.md: dffa009296afde44126725fd65a2fc58977377fc
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: Product subagent named instances
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-18-product-subagent-named-instances.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A Profile can mount one Cordis plugin package in multiple rows, but the Claude Code product provider previously registered every row as `claude-code`. A second row therefore failed as a duplicate before its distinct permission mode, environment, or process-release settings could become usable. Deriving an implicit name from those settings would create a second identity rule, while choosing a provider during a tool call would let model input select deployment authority.
|
||||
|
||||
The existing subagent registry already owns unique provider names, reversible registration, lifecycle events, and holder-owned published runs. The existing `dsh-tool-subagent` configuration already binds one provider name to one model-visible tool name. Product providers need to expose the missing Profile-owned identity without adding another registry or selection protocol.
|
||||
|
||||
## Decision
|
||||
|
||||
The Claude Code provider Config owns a non-empty `providerName` whose default remains `claude-code`. The resolved name is fixed when the plugin row loads and becomes the Provider object's `name`; registration, lookup, lifecycle events, run logs, and HMR removal therefore use the same value. Each mounted row retains its own `permissionMode`, `env`, `disposeGraceMs`, and run resources. The Codex provider still registers its single `codex` default name.
|
||||
|
||||
Profiles may mount multiple Claude Code rows when every row uses a distinct `providerName`. Each `dsh-tool-subagent` row continues to bind its existing `provider` field to that exact name and exposes an independently configured `toolName`. Tool calls carry no provider selector, alias, or permission input. A duplicate provider name fails through the existing `DUPLICATE_PROVIDER` path and leaves the first registration intact.
|
||||
|
||||
Removing one provider row blocks new starts and removes only tools bound to that name. Runs already published by the removed instance remain owned by their holders and settle or dispose independently. Sibling instances remain registered and keep their own environment, native permission mode, cancellation controller, product process, and cleanup grace.
|
||||
|
||||
### Ownership and lifecycle
|
||||
|
||||
| Fact or operation | Owner | Result |
|
||||
| --- | --- | --- |
|
||||
| Provider instance name | Product Provider Config | One immutable registry name per mounted row, with the existing default when omitted |
|
||||
| Name uniqueness and lifecycle events | `ctx.subagents` | Duplicate registration fails; disposal removes only the matching name |
|
||||
| Model-visible tool name and binding | `dsh-tool-subagent` Config | One static tool resolves one configured provider name |
|
||||
| Permission, environment, and process cleanup | One Provider instance | Concurrent runs and sibling instances do not share deployment configuration or run resources |
|
||||
|
||||
## Verification
|
||||
|
||||
Claude Code package tests pin the default and custom names, empty-name rejection, duplicate rollback, actual-name diagnostics, two concurrent instances with different permission modes, environments, and cleanup grace, cancellation isolation, and removal of one instance while its published run remains valid. The official SDK/CLI loopback test runs two named instances in one Host against separate model fixtures and proves independent unload and process-tree quiescence. The public Loader composition mounts two Claude Code rows and two distinct tools without starting either product, while the keyless ACP snapshot pins both static tool schemas and the absence of a dynamic provider parameter.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Derive names from the product or permission mode.** An implicit suffix would make identity change when deployment settings change and could still collide across equivalent rows. The Profile supplies the identity explicitly.
|
||||
|
||||
**Let a tool call choose the provider.** That would make model input select a permission and environment instance. Separate tool rows keep authorization and exposure static in configuration.
|
||||
|
||||
**Create a product-instance catalog or alias registry.** The existing subagent registry already owns names, uniqueness, lookup, events, and disposal. Another directory would duplicate state without a distinct consumer.
|
||||
|
||||
**Automatically rename duplicate rows.** Silent suffixing would make tool bindings and lifecycle diagnostics depend on load order. Duplicate names continue to fail loudly.
|
||||
|
||||
## Consequences
|
||||
|
||||
A Profile can expose several Claude Code tools backed by separate native permission modes and environments while existing configurations continue to resolve `claude-code`. Provider and tool names remain independent configuration facts, so changing one requires updating the binding that refers to it.
|
||||
|
||||
The design adds no runtime renaming, model-visible selector, generated tool name, persistent instance directory, shared process pool, or compatibility alias. Correct multi-instance configurations require unique provider names and unique tool names; duplicate tool-name waiting remains a separate limitation.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: 产品 subagent 命名实例
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-18-product-subagent-named-instances.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Claude Code 产品提供方此前会把每个配置项都注册为 `claude-code`。因此,第二个配置项会在其独立权限模式、环境或进程释放设置可用前因名称重复而失败。根据这些设置隐式派生名称会建立第二套身份规则,而在工具调用期间选择提供方会让模型输入决定部署权限。
|
||||
|
||||
现有 subagent 注册表已经拥有提供方名称唯一性、可逆注册、生命周期事件和由持有方拥有的已发布运行。现有 `dsh-tool-subagent` 配置也已经把一个提供方名称绑定到一个模型可见工具名称。产品提供方只需公开缺失的 Profile 所有身份,无需增加另一套注册表或选择协议。
|
||||
|
||||
## 决策
|
||||
|
||||
Claude Code 提供方 Config 拥有非空的 `providerName`,其默认值仍为 `claude-code`。插件配置项加载时会固定解析后的名称,并把它作为 Provider 对象的 `name`;注册、查找、生命周期事件、运行日志和 HMR(热模块替换)移除因此使用同一个值。每个已挂载配置项保留自己的 `permissionMode`、`env`、`disposeGraceMs` 和运行资源。Codex 提供方仍只注册默认名称 `codex`。
|
||||
|
||||
当每个配置项使用不同的 `providerName` 时,Profile 可以挂载多个 Claude Code 配置项。每个 `dsh-tool-subagent` 配置项继续用已有的 `provider` 字段绑定这个准确名称,并公开独立配置的 `toolName`。工具调用不携带提供方选择器、别名或权限输入。重复提供方名称沿用现有 `DUPLICATE_PROVIDER` 路径失败,而且不会替换第一个注册项。
|
||||
|
||||
移除一个提供方配置项会阻止新的启动,并且只移除绑定到该名称的工具。该实例已经发布的运行仍由其持有方拥有,并会独立结算或 dispose(资源释放)。兄弟实例继续保持注册,并保留各自的环境、原生权限模式、取消控制器、产品进程和清理宽限期。
|
||||
|
||||
### 所有权与生命周期
|
||||
|
||||
| 事实或操作 | 责任方 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 提供方实例名称 | 产品提供方 Config | 每个已挂载配置项拥有一个不可变注册名称;省略时使用现有默认值 |
|
||||
| 名称唯一性与生命周期事件 | `ctx.subagents` | 重复注册失败;资源释放只移除匹配名称 |
|
||||
| 模型可见工具名称与绑定 | `dsh-tool-subagent` Config | 一个静态工具解析一个已配置的提供方名称 |
|
||||
| 权限、环境与进程清理 | 一个提供方实例 | 并发运行与兄弟实例不共享部署配置或运行资源 |
|
||||
|
||||
## 验证
|
||||
|
||||
Claude Code 包测试固定默认与自定义名称、空名称拒绝、重复注册回滚、实际名称诊断、使用不同权限模式、环境与清理宽限期的两个并发实例、取消隔离,以及移除一个实例后其已发布运行仍然有效。官方 SDK/CLI 回环测试会在同一个 Host 中针对独立模型 fixture(测试前置数据)运行两个命名实例,并证明独立卸载与进程树完全停稳。公共 Loader 组合会挂载两个 Claude Code 配置项与两个不同工具,而且不启动任一产品;无密钥 ACP 快照固定两个静态工具 schema,并证明没有动态提供方参数。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**根据产品或权限模式派生名称。** 隐式后缀会让部署设置变化同时改变身份,而且等价配置项之间仍可能冲突。Profile 会显式提供身份。
|
||||
|
||||
**让工具调用选择提供方。** 这会让模型输入选择权限与环境实例。独立工具配置项会让授权与公开范围保持静态配置。
|
||||
|
||||
**建立产品实例目录或别名注册表。** 现有 subagent 注册表已经拥有名称、唯一性、查找、事件和资源释放。另一套目录没有独立消费方,只会复制状态。
|
||||
|
||||
**自动重命名重复配置项。** 静默添加后缀会让工具绑定与生命周期诊断依赖加载顺序。重复名称继续快速失败。
|
||||
|
||||
## 结果
|
||||
|
||||
Profile 可以公开多个由不同原生权限模式与环境支持的 Claude Code 工具,而现有配置仍会解析为 `claude-code`。提供方名称与工具名称继续是彼此独立的配置事实,因此修改其中一项时必须同时更新引用它的绑定。
|
||||
|
||||
本设计不增加运行时改名、模型可见选择器、自动生成的工具名称、持久实例目录、共享进程池或兼容别名。正确的多实例配置要求提供方名称与工具名称都保持唯一;重复工具名称的等待问题仍是独立限制。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: ebef0167d5ecbe0c71d201cd2cc07962ba89c48d
|
||||
config-catalog.zh.md: 4b2ffba0e931c4c515097950e3e69b5744cb5f37
|
||||
config-catalog.md: 33fc261e971f9055f666e5005080e01b31c6d708
|
||||
config-catalog.zh.md: 24ad1fdb5d0d2eb7470785de7b913d7b33f6c9aa
|
||||
|
||||
@@ -2083,6 +2083,8 @@ Requires: `subagents` · `subprocess`
|
||||
```ts config-catalog
|
||||
/** Deployment-owned permission, environment, and process-release settings. */
|
||||
export interface Config {
|
||||
/** Provider name on `ctx.subagents` (default `claude-code`). */
|
||||
providerName?: string
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
@@ -2103,7 +2105,7 @@ export interface Config {
|
||||
export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
|
||||
Source: [`packages/subagent/subagent-claude-code/src/index.ts:37`](../packages/subagent/subagent-claude-code/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-subagent-codex"></a>
|
||||
|
||||
|
||||
@@ -2085,6 +2085,8 @@ export type PermissionPolicy = 'allow' | 'reject'
|
||||
```ts config-catalog
|
||||
/** Deployment-owned permission, environment, and process-release settings. */
|
||||
export interface Config {
|
||||
/** Provider name on `ctx.subagents` (default `claude-code`). */
|
||||
providerName?: string
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
@@ -2105,7 +2107,7 @@ export interface Config {
|
||||
export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
|
||||
```
|
||||
|
||||
来源:[`packages/subagent/subagent-claude-code/src/index.ts:35`](../packages/subagent/subagent-claude-code/src/index.ts)
|
||||
来源:[`packages/subagent/subagent-claude-code/src/index.ts:37`](../packages/subagent/subagent-claude-code/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-subagent-codex"></a>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Keyless twin of product-subagent-both.cordis.yml: preserve both product
|
||||
# Keyless twin of product-subagent-both.cordis.yml: preserve all named product
|
||||
# tools while replacing only the external model adapter.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
@@ -22,10 +22,20 @@
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
config:
|
||||
permissionMode: approve-for-me
|
||||
- id: subagent-claude-code
|
||||
- id: subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
permissionMode: acceptEdits
|
||||
providerName: claude-safe
|
||||
permissionMode: dontAsk
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: safe
|
||||
- id: subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
providerName: claude-bypass
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: bypass
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
@@ -33,10 +43,17 @@
|
||||
toolName: subagent_codex
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-subagent-claude-code
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-bypass
|
||||
toolName: subagent_claude_bypass
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Add both native product providers and the same independent one-shot tool rows
|
||||
# an Agent Preset may contribute. Loading the composition starts neither
|
||||
# product; the scenario pins both model-visible schemas.
|
||||
# Add the native Codex provider, two named Claude Code instances, and the
|
||||
# independent one-shot tool rows an Agent Preset may contribute. Loading the
|
||||
# composition starts neither product; the scenario pins all three schemas.
|
||||
- id: base
|
||||
name: '@deepseek-ai/cordis-plugin-include'
|
||||
config:
|
||||
@@ -11,10 +11,20 @@
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
config:
|
||||
permissionMode: approve-for-me
|
||||
- id: subagent-claude-code
|
||||
- id: subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
permissionMode: acceptEdits
|
||||
providerName: claude-safe
|
||||
permissionMode: dontAsk
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: safe
|
||||
- id: subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
providerName: claude-bypass
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: bypass
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
@@ -22,10 +32,17 @@
|
||||
toolName: subagent_codex
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-subagent-claude-code
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
- id: tool-subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-bypass
|
||||
toolName: subagent_claude_bypass
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
+25
-6
@@ -1,4 +1,4 @@
|
||||
# Test-only composition of both public opt-in providers and one-shot task tools.
|
||||
# Test-only composition of Codex plus two named Claude instances and their tools.
|
||||
# The owning e2e boots this tree but never invokes a model or product process.
|
||||
- id: fixture
|
||||
name: './fixture.ts'
|
||||
@@ -12,10 +12,21 @@
|
||||
- id: subagent-codex
|
||||
name: '@deepseek-ai/dsh-subagent-codex'
|
||||
|
||||
- id: subagent-claude-code
|
||||
- id: subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
permissionMode: acceptEdits
|
||||
providerName: claude-safe
|
||||
permissionMode: dontAsk
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: safe
|
||||
|
||||
- id: subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
providerName: claude-bypass
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
DSH_CLAUDE_INSTANCE: bypass
|
||||
|
||||
- id: tool-subagent-codex
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
@@ -25,11 +36,19 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-claude-code
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
- id: tool-subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-bypass
|
||||
toolName: subagent_claude_bypass
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 'provider-managed'
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ const ctx = await boot(
|
||||
)
|
||||
|
||||
try {
|
||||
const providerNames = ['codex', 'claude-code'] as const
|
||||
const toolNames = ['subagent_codex', 'subagent_claude_code'] as const
|
||||
const providerNames = ['codex', 'claude-safe', 'claude-bypass'] as const
|
||||
const toolNames = [
|
||||
'subagent_codex',
|
||||
'subagent_claude_safe',
|
||||
'subagent_claude_bypass',
|
||||
] as const
|
||||
const providers = providerNames.map((providerName) => {
|
||||
const provider = ctx.subagents.getProvider(providerName)
|
||||
if (provider === undefined) {
|
||||
|
||||
+26
-1
@@ -307,7 +307,32 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent_claude_code",
|
||||
"name": "subagent_claude_bypass",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A short (3-5 word) description of the delegated task, for display."
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
|
||||
},
|
||||
"run_in_background": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"prompt"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subagent_claude_safe",
|
||||
"description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
|
||||
README.md: be3b2262addc487e545fed1f792600a9a5ca24c0
|
||||
README.zh.md: 7ea1b8ca7243790afd387b04d776088cea012718
|
||||
README.md: bc33d97fb6d7224138e01fa86c3ce28b00df08b8
|
||||
README.zh.md: ad7cca3e9da654ae7d4d13739ff81992c7670e04
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns either the strict final answer or safe failure detail through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
This package registers a Profile-named Claude Code subagent provider whose default name is `claude-code`. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns either the strict final answer or safe failure detail through the shared [`dsh-subagent`](../subagent/README.md) result contract.
|
||||
|
||||
## Start and ownership
|
||||
|
||||
@@ -26,6 +26,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `providerName` | `claude-code` | Non-empty registry name on `ctx.subagents`; each mounted instance needs a unique value. |
|
||||
| `env` | `{}` | Explicit SDK/CLI environment layered over the shared credential-scrubbed parent environment. |
|
||||
| `permissionMode` | `dontAsk` | Native non-interactive permission policy fixed for every run from this Provider instance. |
|
||||
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
|
||||
@@ -40,15 +41,24 @@ The provider advertises no optional start-time capabilities and reports `inherit
|
||||
|
||||
Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden.
|
||||
|
||||
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-claude-code` and mount it once on the host plane; loading the provider starts no Claude process until a tool call. Full Agent Presets carry a matching product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
Production `dsh` does not install or mount this optional provider. A Profile that opts in must install `@deepseek-ai/dsh-subagent-claude-code` and may mount one or more host-plane rows with distinct `providerName`, `permissionMode`, and `env` values; omitting `providerName` keeps the `claude-code` default. Loading an instance starts no Claude process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
|
||||
|
||||
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.
|
||||
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider and tool rows, and does not mount duplicate Job services.
|
||||
|
||||
```yaml
|
||||
- id: subagent-claude-code
|
||||
- id: subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
permissionMode: acceptEdits
|
||||
providerName: claude-safe
|
||||
permissionMode: dontAsk
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
|
||||
- id: subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
providerName: claude-bypass
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
|
||||
@@ -58,18 +68,26 @@ The standalone composition below shows the complete explicit capability. A Profi
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
|
||||
- id: tool-subagent-claude-code
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
- id: tool-subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-bypass
|
||||
toolName: subagent_claude_bypass
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
```
|
||||
|
||||
## Product compatibility and evidence
|
||||
|
||||
The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`. Production runs the native `claude` installation. The keyless real-product test uses the SDK-distributed Claude Code 2.1.220 CLI as a deterministic fixture, routed through the same native executable-resolution and Windows batch-shim path; it does not claim compatibility with every independently installed version. Loader composition proves that both product packages coexist without starting either product.
|
||||
The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`. Production runs the native `claude` installation. The keyless real-product test uses the SDK-distributed Claude Code 2.1.220 CLI as a deterministic fixture, routed through the same native executable-resolution and Windows batch-shim path; it does not claim compatibility with every independently installed version. Loader composition proves that two named Claude instances and the Codex package coexist without starting either product.
|
||||
|
||||
The project owner's identity-scoped distribution authorization covers the official SDK and the official CLI/platform payloads declared by each SDK version. [`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) discloses the current optional payload closure without classifying its declared terms as permissive; unrelated non-permissive runtime dependencies continue to fail the notices gate.
|
||||
|
||||
@@ -79,7 +97,7 @@ The project owner's identity-scoped distribution authorization covers the offici
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from the host's native Claude settings and product installation, while the Provider's Profile configuration fixes the query's non-interactive permission mode.
|
||||
The Claude Code child receives the standalone text task as one fresh SDK query. Its workspace is the parent Session cwd; its model, system instructions, tools, sandbox, and authentication come from the host's native Claude settings and product installation, while the selected Provider instance's Profile configuration fixes the query's environment and non-interactive permission mode.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -106,6 +124,7 @@ Append-only: foreground adds one result after the reusable parent prefix, while
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **One fresh query and process per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
|
||||
- **Static instance selection** — Profile rows fix provider names and tool bindings; calls cannot choose a provider dynamically, and every exposed tool needs a unique `toolName`.
|
||||
- **Host settings are intentionally authoritative** — project and user settings can change model, tools, and behavior; the provider does not provide a filtered or hermetic production mode.
|
||||
- **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
|
||||
- **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
本包(package)注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或安全的失败说明。
|
||||
本包(package)注册由 Profile 命名、默认名称为 `claude-code` 的 Claude Code subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK,通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定返回严格的最终答案或安全的失败说明。
|
||||
|
||||
## 启动与所有权
|
||||
|
||||
@@ -26,6 +26,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
| 配置键 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `providerName` | `claude-code` | `ctx.subagents` 中的非空注册名称;每个已挂载实例都需要唯一值。 |
|
||||
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
|
||||
| `permissionMode` | `dontAsk` | 为该提供方实例的每次运行固定原生非交互权限策略。 |
|
||||
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
|
||||
@@ -40,15 +41,24 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
|
||||
|
||||
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-claude-code`,并在 host plane(宿主平面)挂载一次;加载提供方本身不会在工具调用前启动 Claude 进程。完整 Agent Preset 携带对应的产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
生产 `dsh` 不会安装或挂载这个可选提供方。选择启用它的 Profile 必须安装 `@deepseek-ai/dsh-subagent-claude-code`,并可在 host plane(宿主平面)挂载一个或多个具有不同 `providerName`、`permissionMode` 与 `env` 的配置项;省略 `providerName` 时仍使用默认的 `claude-code`。加载实例本身不会在绑定工具调用前启动 Claude 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output` 或 `job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
|
||||
|
||||
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 行,只新增产品提供方行并启用 preset 工具行,禁止重复挂载 Job 服务。
|
||||
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 配置项,新增产品提供方与工具配置项,而且不重复挂载 Job 服务。
|
||||
|
||||
```yaml
|
||||
- id: subagent-claude-code
|
||||
- id: subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
permissionMode: acceptEdits
|
||||
providerName: claude-safe
|
||||
permissionMode: dontAsk
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
|
||||
- id: subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-subagent-claude-code'
|
||||
config:
|
||||
providerName: claude-bypass
|
||||
permissionMode: bypassPermissions
|
||||
env:
|
||||
ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY
|
||||
|
||||
@@ -58,18 +68,26 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
- id: tool-jobs
|
||||
name: '@deepseek-ai/dsh-tool-jobs'
|
||||
|
||||
- id: tool-subagent-claude-code
|
||||
- id: tool-subagent-claude-safe
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-code
|
||||
toolName: subagent_claude_code
|
||||
provider: claude-safe
|
||||
toolName: subagent_claude_safe
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
|
||||
- id: tool-subagent-claude-bypass
|
||||
name: '@deepseek-ai/dsh-tool-subagent'
|
||||
config:
|
||||
provider: claude-bypass
|
||||
toolName: subagent_claude_bypass
|
||||
backgroundMode: one-shot
|
||||
maxDepth: provider-managed
|
||||
```
|
||||
|
||||
## 产品兼容性与证据
|
||||
|
||||
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装。无密钥真实产品测试使用由 SDK 分发的 Claude Code 2.1.220 CLI 作为确定性 fixture(测试前置数据),并通过同一套原生可执行文件解析路径与 Windows batch shim 路径运行;这项测试不声称兼容每个独立安装的版本。Loader 组合证明两个产品包能够共存且不会启动任一产品。
|
||||
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装。无密钥真实产品测试使用由 SDK 分发的 Claude Code 2.1.220 CLI 作为确定性 fixture(测试前置数据),并通过同一套原生可执行文件解析路径与 Windows batch shim 路径运行;这项测试不声称兼容每个独立安装的版本。Loader 组合证明两个命名 Claude 实例可与 Codex 包共存,而且不会启动任一产品。
|
||||
|
||||
限定于项目所有者身份的分发授权涵盖官方 SDK 及每个 SDK 版本声明的官方 CLI/平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会认定其中声明的条款属于宽松许可;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
|
||||
|
||||
@@ -79,7 +97,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自宿主机原生 Claude 设置与产品安装,而提供方的 Profile 配置会固定该 query 的非交互权限模式。
|
||||
Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。它的工作区是父会话 cwd;其模型、系统指令、工具、沙箱和身份验证来自宿主机原生 Claude 设置与产品安装,而所选提供方实例的 Profile 配置会固定该 query 的环境与非交互权限模式。
|
||||
|
||||
#### 对 token 的影响
|
||||
|
||||
@@ -106,6 +124,7 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。
|
||||
## 已知限制与后续工作
|
||||
|
||||
- **每次运行均新建一个 query 和一个进程**:不支持续接、恢复、池化、进度流或产品会话持久化。
|
||||
- **静态选择实例**:Profile 配置项固定提供方名称与工具绑定;调用无法动态选择提供方,而且每个公开工具都需要唯一的 `toolName`。
|
||||
- **宿主设置有意保持权威**:项目和用户设置可以改变模型、工具与行为;本提供方不提供经过筛选或与宿主环境隔离的生产模式。
|
||||
- **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
|
||||
- **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Fixed Claude Code one-shot subagent provider. Every accepted run invokes
|
||||
* the official Agent SDK in the delegating Session's workspace and places
|
||||
* the SDK-spawned real CLI under the shared subprocess owner.
|
||||
* Profile-named Claude Code one-shot subagent provider. Every accepted run
|
||||
* invokes the official Agent SDK in the delegating Session's workspace and
|
||||
* places the SDK-spawned real CLI under the shared subprocess owner.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-claude-code
|
||||
*/
|
||||
@@ -29,10 +29,14 @@ import {
|
||||
export const name = 'subagent-claude-code'
|
||||
export const inject = ['subagents', 'subprocess']
|
||||
|
||||
const DEFAULT_PROVIDER_NAME = 'claude-code'
|
||||
|
||||
/* jscpd:ignore-start -- sibling product providers intentionally expose
|
||||
* overlapping deployment-owned fields without adding a shared config owner. */
|
||||
/** Deployment-owned permission, environment, and process-release settings. */
|
||||
export interface Config {
|
||||
/** Provider name on `ctx.subagents` (default `claude-code`). */
|
||||
providerName?: string
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
@@ -50,6 +54,7 @@ export interface Config {
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
providerName: z.string().min(1).default(DEFAULT_PROVIDER_NAME),
|
||||
env: z.dict(z.string()).default({}),
|
||||
permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES])
|
||||
.default(DEFAULT_CLAUDE_CODE_PERMISSION_MODE),
|
||||
@@ -62,11 +67,11 @@ type ResolvedConfig = Required<Config>
|
||||
/* jscpd:ignore-start -- Cordis registration and shared-seam plumbing mirror
|
||||
* the Codex sibling; each product's lifecycle remains package-private. */
|
||||
class ClaudeCodeProvider implements SubagentProvider {
|
||||
readonly name = 'claude-code'
|
||||
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
|
||||
readonly inheritsParentContext = false
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
private readonly ctx: Context,
|
||||
private readonly config: ResolvedConfig,
|
||||
) {}
|
||||
@@ -96,7 +101,7 @@ class ClaudeCodeProvider implements SubagentProvider {
|
||||
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),
|
||||
onError: (error, stopReason) => {
|
||||
this.ctx.logger.warn(
|
||||
`subagent-claude-code: child run failed (${stopReason}): ${error.message}`,
|
||||
`subagent-claude-code "${this.name}": child run failed (${stopReason}): ${error.message}`,
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -105,12 +110,13 @@ class ClaudeCodeProvider implements SubagentProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the fixed `claude-code` provider.
|
||||
* Register one Profile-named Claude Code provider.
|
||||
* @param ctx - context carrying shared subagent and subprocess services.
|
||||
* @param config - permission mode, child environment, and disposal grace.
|
||||
* @param config - registry name, permission mode, child environment, and disposal grace.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const resolved: ResolvedConfig = {
|
||||
providerName: config.providerName ?? DEFAULT_PROVIDER_NAME,
|
||||
env: config.env as Record<string, string>,
|
||||
permissionMode: config.permissionMode ?? DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
|
||||
disposeGraceMs: config.disposeGraceMs as number,
|
||||
@@ -125,6 +131,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
`subagent-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`,
|
||||
)
|
||||
}
|
||||
ctx.subagents.registerProvider(new ClaudeCodeProvider(ctx, resolved))
|
||||
if (resolved.providerName.length === 0) {
|
||||
throw new TypeError('subagent-claude-code providerName must be non-empty')
|
||||
}
|
||||
ctx.subagents.registerProvider(new ClaudeCodeProvider(
|
||||
resolved.providerName,
|
||||
ctx,
|
||||
resolved,
|
||||
))
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
@@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml')
|
||||
const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('product-provider public Loader composition', () => {
|
||||
it('loads both opt-in packages, one-shot task tools, and job controls without starting either product', async () => {
|
||||
it('loads two named Claude instances, their tools, and Codex without starting either product', async () => {
|
||||
const { stdout, stderr } = await runLoaderSmoke({
|
||||
label: 'product-provider Loader composition',
|
||||
tempDirPrefix: 'dsh-product-provider-loader-',
|
||||
@@ -31,7 +31,7 @@ describe('product-provider public Loader composition', () => {
|
||||
|
||||
expect(stderr).toBe('')
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
registeredProviders: ['codex', 'claude-code'],
|
||||
registeredProviders: ['codex', 'claude-safe', 'claude-bypass'],
|
||||
providers: [
|
||||
{
|
||||
name: 'codex',
|
||||
@@ -44,7 +44,17 @@ describe('product-provider public Loader composition', () => {
|
||||
inheritsParentContext: false,
|
||||
},
|
||||
{
|
||||
name: 'claude-code',
|
||||
name: 'claude-safe',
|
||||
capabilities: {
|
||||
outputSchema: false,
|
||||
depthLimit: false,
|
||||
toolFilter: false,
|
||||
persona: false,
|
||||
},
|
||||
inheritsParentContext: false,
|
||||
},
|
||||
{
|
||||
name: 'claude-bypass',
|
||||
capabilities: {
|
||||
outputSchema: false,
|
||||
depthLimit: false,
|
||||
@@ -61,7 +71,12 @@ describe('product-provider public Loader composition', () => {
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
{
|
||||
name: 'subagent_claude_code',
|
||||
name: 'subagent_claude_safe',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
{
|
||||
name: 'subagent_claude_bypass',
|
||||
parameterNames: ['description', 'prompt', 'run_in_background'],
|
||||
required: ['description', 'prompt'],
|
||||
},
|
||||
|
||||
@@ -119,19 +119,23 @@ interface RealHarness {
|
||||
readonly handles: SubprocessHandle[]
|
||||
readonly spawnSpecs: SubprocessSpawnSpec[]
|
||||
readonly parent: Agent
|
||||
readonly providerName: string
|
||||
readonly workspace: string
|
||||
readonly env: Record<string, string>
|
||||
readonly executable: string
|
||||
}
|
||||
|
||||
async function realHarness(
|
||||
behavior: MessagesBehavior,
|
||||
permissionMode?: ClaudeCodePermissionMode,
|
||||
nativeAllow: readonly string[] = [],
|
||||
): Promise<{
|
||||
readonly harness: RealHarness
|
||||
interface RealInstanceFixture {
|
||||
readonly fixture: MessagesFixture
|
||||
}> {
|
||||
readonly workspace: string
|
||||
readonly env: Record<string, string>
|
||||
readonly executable: string
|
||||
}
|
||||
|
||||
async function realInstanceFixture(
|
||||
behavior: MessagesBehavior,
|
||||
nativeAllow: readonly string[] = [],
|
||||
): Promise<RealInstanceFixture> {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-claude-code-real-'))
|
||||
roots.push(root)
|
||||
const workspace = join(root, 'workspace')
|
||||
@@ -176,6 +180,16 @@ async function realHarness(
|
||||
ALL_PROXY: '',
|
||||
NO_PROXY: '127.0.0.1,localhost',
|
||||
}
|
||||
return { fixture, workspace, env, executable }
|
||||
}
|
||||
|
||||
interface RealRuntime {
|
||||
readonly ctx: Context
|
||||
readonly handles: SubprocessHandle[]
|
||||
readonly spawnSpecs: SubprocessSpawnSpec[]
|
||||
}
|
||||
|
||||
async function realRuntime(): Promise<RealRuntime> {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
@@ -189,18 +203,42 @@ async function realHarness(
|
||||
handles.push(handle)
|
||||
return handle
|
||||
})
|
||||
return { ctx, handles, spawnSpecs }
|
||||
}
|
||||
|
||||
async function realHarness(
|
||||
behavior: MessagesBehavior,
|
||||
permissionMode?: ClaudeCodePermissionMode,
|
||||
nativeAllow: readonly string[] = [],
|
||||
providerName = 'claude-code',
|
||||
): Promise<{
|
||||
readonly harness: RealHarness
|
||||
readonly fixture: MessagesFixture
|
||||
}> {
|
||||
const instance = await realInstanceFixture(behavior, nativeAllow)
|
||||
const { ctx, handles, spawnSpecs } = await realRuntime()
|
||||
await ctx.plugin(claudeCode, {
|
||||
env,
|
||||
providerName,
|
||||
env: instance.env,
|
||||
...permissionMode === undefined ? {} : { permissionMode },
|
||||
disposeGraceMs: 3_000,
|
||||
})
|
||||
const parent = {
|
||||
id: 'real-parent',
|
||||
session: { header: { cwd: workspace } },
|
||||
session: { header: { cwd: instance.workspace } },
|
||||
} as unknown as Agent
|
||||
return {
|
||||
harness: { ctx, handles, spawnSpecs, parent, workspace, env, executable },
|
||||
fixture,
|
||||
harness: {
|
||||
ctx,
|
||||
handles,
|
||||
spawnSpecs,
|
||||
parent,
|
||||
providerName,
|
||||
workspace: instance.workspace,
|
||||
env: instance.env,
|
||||
executable: instance.executable,
|
||||
},
|
||||
fixture: instance.fixture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +259,7 @@ function startRequest(
|
||||
prompt: string,
|
||||
signal = new AbortController().signal,
|
||||
) {
|
||||
return harness.ctx.subagents.start('claude-code', {
|
||||
return harness.ctx.subagents.start(harness.providerName, {
|
||||
prompt: [{ type: 'text', text: prompt }],
|
||||
parent: harness.parent,
|
||||
signal,
|
||||
@@ -294,6 +332,80 @@ describe('real Claude Agent SDK 0.3.220 and its distributed Claude Code 2.1.220
|
||||
await expectQuiescent(harness.handles)
|
||||
})
|
||||
|
||||
it('runs two named instances concurrently and unloads one without revoking its run', async () => {
|
||||
const safeInstance = await realInstanceFixture({ kind: 'hold' })
|
||||
const bypassInstance = await realInstanceFixture({
|
||||
kind: 'complete',
|
||||
text: 'NAMED_BYPASS_RESULT',
|
||||
})
|
||||
const { ctx, handles, spawnSpecs } = await realRuntime()
|
||||
const safeFiber = await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-safe',
|
||||
env: safeInstance.env,
|
||||
permissionMode: 'dontAsk',
|
||||
disposeGraceMs: 3_000,
|
||||
})
|
||||
const bypassFiber = await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-bypass',
|
||||
env: bypassInstance.env,
|
||||
permissionMode: 'bypassPermissions',
|
||||
disposeGraceMs: 3_000,
|
||||
})
|
||||
const safeParent = {
|
||||
id: 'safe-parent',
|
||||
session: { header: { cwd: safeInstance.workspace } },
|
||||
} as unknown as Agent
|
||||
const bypassParent = {
|
||||
id: 'bypass-parent',
|
||||
session: { header: { cwd: bypassInstance.workspace } },
|
||||
} as unknown as Agent
|
||||
const safeController = new AbortController()
|
||||
|
||||
const [safeRun, bypassRun] = await Promise.all([
|
||||
ctx.subagents.start('claude-safe', {
|
||||
prompt: [{ type: 'text', text: 'Hold the safe instance.' }],
|
||||
parent: safeParent,
|
||||
signal: safeController.signal,
|
||||
}),
|
||||
ctx.subagents.start('claude-bypass', {
|
||||
prompt: [{ type: 'text', text: 'Complete the bypass instance.' }],
|
||||
parent: bypassParent,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
])
|
||||
await safeInstance.fixture.requestStarted
|
||||
await safeFiber.dispose()
|
||||
expect(ctx.subagents.list()).toEqual(['claude-bypass'])
|
||||
await expect(ctx.subagents.start('claude-safe', {
|
||||
prompt: [{ type: 'text', text: 'This start must fail.' }],
|
||||
parent: safeParent,
|
||||
signal: new AbortController().signal,
|
||||
})).rejects.toMatchObject({ code: 'NO_PROVIDER' })
|
||||
|
||||
await expect(bypassRun.result).resolves.toEqual({
|
||||
output: [{ type: 'text', text: 'NAMED_BYPASS_RESULT' }],
|
||||
stopReason: 'completed',
|
||||
})
|
||||
safeController.abort(new Error('cancel only the published safe run'))
|
||||
await expect(safeRun.result).resolves.toEqual({
|
||||
output: [],
|
||||
stopReason: 'aborted',
|
||||
})
|
||||
await Promise.all([safeRun.dispose(), bypassRun.dispose()])
|
||||
expect(safeInstance.fixture.requests).toHaveLength(1)
|
||||
expect(bypassInstance.fixture.requests).toHaveLength(1)
|
||||
expect(safeInstance.fixture.requests[0]?.body.messages)
|
||||
.not.toEqual(bypassInstance.fixture.requests[0]?.body.messages)
|
||||
expect(spawnSpecs.map(spec => spec.env?.CLAUDE_CONFIG_DIR).sort())
|
||||
.toEqual([
|
||||
safeInstance.env.CLAUDE_CONFIG_DIR,
|
||||
bypassInstance.env.CLAUDE_CONFIG_DIR,
|
||||
].sort())
|
||||
await expectQuiescent(handles)
|
||||
await bypassFiber.dispose()
|
||||
expect(ctx.subagents.list()).toEqual([])
|
||||
})
|
||||
|
||||
it('maps a real CLI process failure to error', async () => {
|
||||
const { harness, fixture } = await realHarness({ kind: 'hold' })
|
||||
const run = await startRequest(harness, 'Exercise the failure path.')
|
||||
|
||||
@@ -312,7 +312,7 @@ describe('task admission and package contracts', () => {
|
||||
.toThrow('must not be empty')
|
||||
})
|
||||
|
||||
it('registers one fixed descriptor, validates config, and unregisters on HMR', async () => {
|
||||
it('registers the default descriptor, validates config, and unregisters on HMR', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
@@ -343,7 +343,126 @@ describe('task admission and package contracts', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('keeps named instances, runs, and HMR ownership isolated', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
const safeChild = fakeChild()
|
||||
const bypassChild = fakeChild()
|
||||
const spawnSpecs: SubprocessSpawnSpec[] = []
|
||||
vi.spyOn(ctx.subprocess, 'resolveExecutable')
|
||||
.mockResolvedValue('/native/claude')
|
||||
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
|
||||
spawnSpecs.push(spec)
|
||||
return spec.env?.DSH_CLAUDE_INSTANCE === 'safe'
|
||||
? safeChild.handle
|
||||
: bypassChild.handle
|
||||
})
|
||||
const queryOptions: Options[] = []
|
||||
queryMock.mockImplementation(({ options }) => {
|
||||
queryOptions.push(options)
|
||||
options.spawnClaudeCodeProcess!(sdkSpawnOptions({
|
||||
command: options.pathToClaudeCodeExecutable!,
|
||||
cwd: options.cwd!,
|
||||
env: options.env!,
|
||||
signal: options.abortController!.signal,
|
||||
}))
|
||||
return options.permissionMode === 'dontAsk'
|
||||
? waitingQuery(options.abortController!.signal)
|
||||
: queryFrom([success('bypass answer')])
|
||||
})
|
||||
|
||||
const added: string[] = []
|
||||
const started: string[] = []
|
||||
const ended: string[] = []
|
||||
const removed: string[] = []
|
||||
ctx.on('subagent/provider-added', provider => void added.push(provider.name))
|
||||
ctx.on('subagent/start', info => void started.push(info.provider))
|
||||
ctx.on('subagent/end', info => void ended.push(info.provider))
|
||||
ctx.on('subagent/provider-removed', providerName => void removed.push(providerName))
|
||||
const safeFiber = await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-safe',
|
||||
env: { DSH_CLAUDE_INSTANCE: 'safe' },
|
||||
permissionMode: 'dontAsk',
|
||||
disposeGraceMs: 11,
|
||||
})
|
||||
const bypassFiber = await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-bypass',
|
||||
env: { DSH_CLAUDE_INSTANCE: 'bypass' },
|
||||
permissionMode: 'bypassPermissions',
|
||||
disposeGraceMs: 29,
|
||||
})
|
||||
expect(ctx.subagents.list()).toEqual(['claude-safe', 'claude-bypass'])
|
||||
expect(added).toEqual(['claude-safe', 'claude-bypass'])
|
||||
|
||||
const safeController = new AbortController()
|
||||
const [safeRun, bypassRun] = await Promise.all([
|
||||
ctx.subagents.start('claude-safe', request(undefined, safeController.signal)),
|
||||
ctx.subagents.start('claude-bypass', request()),
|
||||
])
|
||||
await safeFiber.dispose()
|
||||
expect(ctx.subagents.list()).toEqual(['claude-bypass'])
|
||||
expect(removed).toEqual(['claude-safe'])
|
||||
await expect(ctx.subagents.start('claude-safe', request()))
|
||||
.rejects.toMatchObject({ code: 'NO_PROVIDER' })
|
||||
|
||||
await expect(bypassRun.result).resolves.toEqual({
|
||||
output: [{ type: 'text', text: 'bypass answer' }],
|
||||
stopReason: 'completed',
|
||||
})
|
||||
safeController.abort(new Error('stop only the safe instance'))
|
||||
await expect(safeRun.result).resolves.toEqual({
|
||||
output: [],
|
||||
stopReason: 'aborted',
|
||||
})
|
||||
expect(queryOptions.map(options => ({
|
||||
instance: options.env?.DSH_CLAUDE_INSTANCE,
|
||||
permissionMode: options.permissionMode,
|
||||
}))).toEqual([
|
||||
{ instance: 'safe', permissionMode: 'dontAsk' },
|
||||
{ instance: 'bypass', permissionMode: 'bypassPermissions' },
|
||||
])
|
||||
expect(spawnSpecs.map(spec => ({
|
||||
instance: spec.env?.DSH_CLAUDE_INSTANCE,
|
||||
graceMs: spec.graceMs,
|
||||
}))).toEqual([
|
||||
{ instance: 'safe', graceMs: 11 },
|
||||
{ instance: 'bypass', graceMs: 29 },
|
||||
])
|
||||
|
||||
await Promise.all([safeRun.dispose(), bypassRun.dispose()])
|
||||
expect([...started].sort()).toEqual(['claude-bypass', 'claude-safe'])
|
||||
expect([...ended].sort()).toEqual(['claude-bypass', 'claude-safe'])
|
||||
expect(safeChild.terminate).toHaveBeenCalledOnce()
|
||||
expect(bypassChild.terminate).toHaveBeenCalledOnce()
|
||||
await bypassFiber.dispose()
|
||||
expect(removed).toEqual(['claude-safe', 'claude-bypass'])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects duplicate provider names without replacing the first instance', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SubagentRuntime)
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
const firstFiber = await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-duplicate',
|
||||
})
|
||||
const first = ctx.subagents.getProvider('claude-duplicate')
|
||||
await expect(ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-duplicate',
|
||||
permissionMode: 'bypassPermissions',
|
||||
})).rejects.toMatchObject({ code: 'DUPLICATE_PROVIDER' })
|
||||
expect(ctx.subagents.getProvider('claude-duplicate')).toBe(first)
|
||||
expect(ctx.subagents.list()).toEqual(['claude-duplicate'])
|
||||
await firstFiber.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('accepts only the five fixed non-interactive permission modes', () => {
|
||||
expect(claudeCode.Config({}).providerName).toBe('claude-code')
|
||||
expect(claudeCode.Config({ providerName: 'claude-safe' }).providerName)
|
||||
.toBe('claude-safe')
|
||||
expect(() => claudeCode.Config({ providerName: '' })).toThrow()
|
||||
expect(claudeCode.Config({}).permissionMode)
|
||||
.toBe(DEFAULT_CLAUDE_CODE_PERMISSION_MODE)
|
||||
for (const permissionMode of CLAUDE_CODE_PERMISSION_MODES) {
|
||||
@@ -361,6 +480,13 @@ describe('task admission and package contracts', () => {
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
claudeCode.apply(ctx, { env: {}, disposeGraceMs: 3_000 })
|
||||
expect(ctx.subagents.getProvider('claude-code')).toBeDefined()
|
||||
expect(() => {
|
||||
claudeCode.apply(ctx, {
|
||||
providerName: '',
|
||||
env: {},
|
||||
disposeGraceMs: 3_000,
|
||||
})
|
||||
}).toThrow('providerName must be non-empty')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -375,6 +501,7 @@ describe('task admission and package contracts', () => {
|
||||
.mockResolvedValue('/native/claude')
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
|
||||
await ctx.plugin(claudeCode, {
|
||||
providerName: 'claude-diagnostic',
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'provider-fake-key',
|
||||
CLAUDE_CONFIG_DIR: '/private/tmp/dsh-claude-code-unit-config',
|
||||
@@ -384,7 +511,7 @@ describe('task admission and package contracts', () => {
|
||||
disposeGraceMs: 29,
|
||||
})
|
||||
|
||||
await expect(ctx.subagents.start('claude-code', {
|
||||
await expect(ctx.subagents.start('claude-diagnostic', {
|
||||
...request(),
|
||||
parent: {
|
||||
id: 'parent-without-cwd',
|
||||
@@ -396,11 +523,11 @@ describe('task admission and package contracts', () => {
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
resolveExecutable.mockRejectedValueOnce(new Error('claude missing from PATH'))
|
||||
await expect(ctx.subagents.start('claude-code', request()))
|
||||
await expect(ctx.subagents.start('claude-diagnostic', request()))
|
||||
.rejects.toThrow('claude missing from PATH')
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
const run = await ctx.subagents.start('claude-code', request())
|
||||
const run = await ctx.subagents.start('claude-diagnostic', request())
|
||||
child.settle({ exitCode: 9, signal: null })
|
||||
child.stdout.end()
|
||||
await expect(run.result).resolves.toEqual({
|
||||
@@ -408,7 +535,7 @@ describe('task admission and package contracts', () => {
|
||||
stopReason: 'error',
|
||||
})
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
'subagent-claude-code: child run failed (error):',
|
||||
'subagent-claude-code "claude-diagnostic": child run failed (error):',
|
||||
))
|
||||
expect(resolveExecutable).toHaveBeenCalledWith(
|
||||
'claude',
|
||||
|
||||
Reference in New Issue
Block a user