feat(agent-presets): add Codex and Claude Code subagent tools

This commit is contained in:
pku-xht
2026-08-10 12:45:05 +08:00
parent e637f95d78
commit b1d67a6935
45 changed files with 1708 additions and 45 deletions

View File

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

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, the shared [`agent-default-model`](../../core/agent-default-model/README.md) selection, tools, persistence, policy, settings/credentials, repository Plugins, telemetry, and host-level subagent providers — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Codex and Claude Code providers load dormant; Agent Presets independently decide whether their agent contributes either model-facing delegation tool. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code.
The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、共享的 [`agent-default-model`](../../core/agent-default-model/README.md) 选择、工具、持久化、策略、settingscredentials、repository 插件、遥测与宿主级 subagent provider——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。Codex 与 Claude Code provider 以休眠状态加载Agent Preset 分别决定自己的 agent 是否贡献任一面向模型的委派工具。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 APIprofile 组合器通过 manifest元数据清单`dsh.bundle.patch` 字段解析 patch绝不通过代码。
行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。

View File

@@ -288,6 +288,15 @@
config:
providerName: fork
# Product providers stay on the host plane because the registry is a
# process singleton. Agent presets decide whether their own model sees the
# matching delegation tools; loading either provider starts no product.
- id: subagent-codex
name: '@deepseek-ai/dsh-subagent-codex'
- id: subagent-claude-code
name: '@deepseek-ai/dsh-subagent-claude-code'
# Continuable background children are selected per delegation tool. The
# separately loaded follow-up tool registers the one global `send_message`.
- id: tool-subagent-control

View File

@@ -76,6 +76,8 @@
"@deepseek-ai/dsh-spill-local": "workspace:^",
"@deepseek-ai/dsh-spill-policy": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-claude-code": "workspace:^",
"@deepseek-ai/dsh-subagent-codex": "workspace:^",
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",

View File

@@ -13,7 +13,10 @@ import { entryListSchema } from '@cordisjs/plugin-include'
describe('dsh-base bundle', () => {
it('declares a parseable patch list through the dsh.bundle.patch manifest field', () => {
const root = fileURLToPath(new URL('..', import.meta.url))
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { dsh?: { bundle?: { patch?: string } } }
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
dependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string } }
}
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), { schema: entryListSchema })
expect(Array.isArray(parsed)).toBe(true)
@@ -21,5 +24,11 @@ describe('dsh-base bundle', () => {
const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap(patch => patch.insert ?? [])
expect(rows.length).toBeGreaterThan(50)
expect(rows.some(row => row.id === 'agent-loop')).toBe(true)
expect(rows.filter(row => row.id === 'subagent-codex')).toHaveLength(1)
expect(rows.filter(row => row.id === 'subagent-claude-code')).toHaveLength(1)
expect(manifest.dependencies).toMatchObject({
'@deepseek-ai/dsh-subagent-codex': 'workspace:^',
'@deepseek-ai/dsh-subagent-claude-code': 'workspace:^',
})
})
})

View File

@@ -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: 222cf796f71f8dc0dc2c06f7f32bab70ded6ab43
README.zh.md: 9334820a591cbfcb8dc2046dc3a78201ba193aab
README.md: 5e3138b9211b01de9096fa1b8e8b68321aad0c7d
README.zh.md: cc536d13d58e08efc77f4f7b4374c8a1af8c5caa

View File

@@ -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, starts the SDK-distributed Claude Code CLI through the shared subprocess service, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
This package registers the fixed `claude-code` subagent provider. Each accepted run invokes the official Claude Agent SDK in the delegating Session's workspace, resolves the native `claude` executable through the shared subprocess service, submits one self-contained text task, and returns only the final answer through the shared [`dsh-subagent`](../subagent/README.md) result contract.
## Start and ownership
@@ -29,9 +29,9 @@ The provider advertises no optional start-time capabilities and reports `inherit
| `env` | `{}` | Explicit SDK/CLI environment layered over the shared credential-scrubbed parent environment. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
Production uses the Claude Code CLI supplied by `@anthropic-ai/claude-agent-sdk` and the host's native settings and authentication. 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 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`. 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.
Install this package and add the following rows to your own `cordis.yml`. Shipped CLI configurations do not load this provider or expose `subagent_claude_code` by default.
Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. A custom host composition can still use both rows directly.
```yaml
- id: subagent-claude-code
@@ -42,6 +42,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
@@ -51,7 +52,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe
## Product compatibility and evidence
The runtime dependency is pinned to `@anthropic-ai/claude-agent-sdk@0.3.220`, whose platform optional dependency supplies Claude Code 2.1.220. Required evidence exercises that official distribution through a keyless loopback product path and a credentialed DeepSeek path, while Loader composition proves that both opt-in 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 SDK's platform optional payload remains in the current installation closure and is tracked as a separate distribution follow-up. Required evidence exercises the compatible native product through a keyless loopback path and a credentialed DeepSeek path, while Loader composition proves that both product packages 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.
@@ -89,7 +90,7 @@ Append-only: the new tool result follows the reusable parent request prefix.
- **One fresh query and process per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
- **Host settings are intentionally authoritative** — project and user settings can change model, tools, and behavior; the provider does not provide a filtered or hermetic production mode.
- **Product installation and account state remain native** — an incompatible SDK payload, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow.
- **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.
- **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending.
- **Final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
本包package注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK通过共享子进程服务启动 SDK 分发的 Claude Code CLI,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
本包package注册固定的 `claude-code` subagent 提供方。每次接受运行请求后,它都会在发起委托的会话工作区中调用官方 Claude Agent SDK通过共享子进程服务解析原生 `claude` 可执行文件,提交一个自包含的文本任务,并通过共享的 [`dsh-subagent`](../subagent/README.md) 结果约定仅返回最终答案。
## 启动与所有权
@@ -29,9 +29,9 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
生产环境使用 `@anthropic-ai/claude-agent-sdk` 提供的 Claude Code CLI以及宿主机原生设置与身份验证。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH``HOME` 等普通环境变量仍会被继承。
生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH``HOME` 等普通环境变量仍会被继承。
请安装此包,并将以下配置项添加到你自己的 `cordis.yml`。正式 CLI 配置默认不会加载此提供方,也不会暴露 `subagent_claude_code`
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`自定义宿主组装仍可直接使用两条配置行。
```yaml
- id: subagent-claude-code
@@ -42,6 +42,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
- id: tool-subagent-claude-code
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: claude-code
toolName: subagent_claude_code
@@ -51,7 +52,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
## 产品兼容性与证据
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其平台可选依赖提供 Claude Code 2.1.220。强制证据会通过无密钥回环产品路径与带密钥 DeepSeek 路径运行该官方发行版,而 Loader 组合则证明两个选择启用的产品包能够共存且不会启动任一产品。
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`。生产运行使用原生 `claude` 安装SDK 的平台可选载荷仍处于当前安装闭包,并作为独立分发后续项跟踪。强制证据会通过无密钥回环路径与带密钥 DeepSeek 路径运行兼容的原生产品,而 Loader 组合则证明两个产品包能够共存且不会启动任一产品。
项目所有者按身份范围授权分发官方 SDK 及每个 SDK 版本声明的官方 CLI平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会把其声明条款归类为宽松许可证;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。
@@ -89,7 +90,7 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务
- **每次运行均新建一个 query 和一个进程**:不支持续接、恢复、池化、进度流或产品会话持久化。
- **宿主设置有意保持权威**:项目和用户设置可以改变模型、工具与行为;本提供方不提供经过筛选或与宿主环境隔离的生产模式。
- **产品安装与账户状态仍由原生机制管理**:不兼容的 SDK 载荷、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
- **产品安装与账户状态仍由原生机制管理**`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。
- **没有人工交互路径**`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。
- **仅返回最终文本**推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。

View File

@@ -59,19 +59,25 @@ class ClaudeCodeProvider implements SubagentProvider {
private readonly config: ResolvedConfig,
) {}
start(request: ResolvedSubagentStartRequest) {
async start(request: ResolvedSubagentStartRequest) {
const parentCwd = request.parent.session.header.cwd
if (parentCwd === undefined) {
throw new Error(
'subagent-claude-code: no working directory for the child — delegate from a parent session that has one',
)
}
const executable = await this.ctx.subprocess.resolveExecutable(
'claude',
this.config.env,
request.signal,
)
const spec: ClaudeCodeRunSpec = {
cwd: resolveChildCwd(
'subagent-claude-code',
undefined,
parentCwd,
),
executable,
env: this.config.env,
disposeGraceMs: this.config.disposeGraceMs,
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),

View File

@@ -6,6 +6,7 @@
*/
import { EventEmitter } from 'node:events'
import { extname } from 'node:path'
import type {
SpawnedProcess,
SpawnOptions,
@@ -40,17 +41,23 @@ export function sdkEnvironmentOverlay(
* Translate one official SDK spawn request to the shared process owner.
* @param options - command, arguments, workspace, environment, and forwarded signal from the SDK.
* @param graceMs - process-tree termination grace.
* @param platform - host platform selecting the Windows batch-shim boundary.
* @returns the fully explicit shared subprocess request.
*/
export function claudeSpawnSpec(
options: SpawnOptions,
graceMs: number,
platform: NodeJS.Platform = process.platform,
): SubprocessSpawnSpec {
if (options.cwd === undefined || options.cwd.length === 0) {
throw new Error('subagent-claude-code: SDK spawn request omitted its workspace')
}
const extension = extname(options.command).toLowerCase()
const argv = platform === 'win32' && (extension === '.cmd' || extension === '.bat')
? ['cmd.exe', '/d', '/s', '/c', options.command, ...options.args]
: [options.command, ...options.args]
return {
argv: [options.command, ...options.args],
argv,
cwd: options.cwd,
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
graceMs,

View File

@@ -44,6 +44,8 @@ export const DEFAULT_DISPOSE_GRACE_MS = 3_000
export interface ClaudeCodeRunSpec {
/** Parent Session workspace supplied to the SDK and real CLI. */
readonly cwd: string
/** Exact native Claude Code executable resolved from the host PATH. */
readonly executable: string
/** Explicit deployment/test environment layered after shared scrubbing. */
readonly env: Record<string, string>
/** Subprocess termination grace passed to the shared process-tree owner. */
@@ -180,6 +182,7 @@ export function claudeQueryOptions(
return {
abortController: controller,
cwd: spec.cwd,
pathToClaudeCodeExecutable: spec.executable,
env: { ...scrubbedParentEnv(), ...spec.env },
persistSession: false,
disallowedTools: ['AskUserQuestion'],

View File

@@ -7,7 +7,7 @@ import {
rmSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { delimiter, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from 'cordis'
@@ -87,6 +87,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
]) mkdirSync(directory)
const env = {
PATH: `${dirname(claudeBin)}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',

View File

@@ -1,13 +1,15 @@
import { execFile } from 'node:child_process'
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { delimiter, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import type {
@@ -19,7 +21,7 @@ import { Context } from 'cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
import {
@@ -98,9 +100,11 @@ afterEach(async () => {
interface RealHarness {
readonly ctx: Context
readonly handles: SubprocessHandle[]
readonly spawnSpecs: SubprocessSpawnSpec[]
readonly parent: Agent
readonly workspace: string
readonly env: Record<string, string>
readonly executable: string
}
async function realHarness(behavior: MessagesBehavior): Promise<{
@@ -112,9 +116,14 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const workspace = join(root, 'workspace')
const claudeConfig = join(root, 'claude-config')
const xdgConfig = join(root, 'xdg')
const nativeBin = join(root, 'native-bin')
mkdirSync(workspace)
mkdirSync(claudeConfig)
mkdirSync(xdgConfig)
mkdirSync(nativeBin)
const executable = join(nativeBin, process.platform === 'win32' ? 'claude.exe' : 'claude')
if (process.platform === 'win32') copyFileSync(claudeBin, executable)
else symlinkSync(claudeBin, executable)
writeFileSync(
join(claudeConfig, 'settings.json'),
`${JSON.stringify({ model: settingsModel }, null, 2)}\n`,
@@ -122,6 +131,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const fixture = await startMessagesFixture(behavior)
fixtures.push(fixture)
const env = {
PATH: `${nativeBin}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_API_KEY: fakeKey,
ANTHROPIC_BASE_URL: fixture.baseUrl,
CLAUDE_CONFIG_DIR: claudeConfig,
@@ -141,8 +151,10 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
const handles: SubprocessHandle[] = []
const spawnSpecs: SubprocessSpawnSpec[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
spawnSpecs.push(spec)
const handle = spawn(spec)
handles.push(handle)
return handle
@@ -153,7 +165,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
session: { header: { cwd: workspace } },
} as unknown as Agent
return {
harness: { ctx, handles, parent, workspace, env },
harness: { ctx, handles, spawnSpecs, parent, workspace, env, executable },
fixture,
}
}
@@ -195,7 +207,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
expect(sdkPackage.version).toBe('0.3.220')
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
const version = await execFileAsync(claudeBin, ['--version'], {
const version = await execFileAsync(harness.executable, ['--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
@@ -212,6 +224,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
message.type === 'system' && message.subtype === 'init',
)
expect(initMessage?.claude_code_version).toBe('2.1.220')
expect(harness.spawnSpecs[0]?.argv[0]).toBe(harness.executable)
expect(fixture.requests).toHaveLength(1)
const recorded = fixture.requests[0]!

View File

@@ -248,6 +248,7 @@ function fakeRun(
const options: FakeRun['options'] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: { ANTHROPIC_API_KEY: 'fake-key' },
disposeGraceMs: 5,
spawn: (spawnSpec) => {
@@ -331,6 +332,8 @@ describe('task admission and package contracts', () => {
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
.mockImplementation(() => child.handle)
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable')
.mockResolvedValue('/native/claude')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
await ctx.plugin(claudeCode, {
env: {
@@ -352,6 +355,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()))
.rejects.toThrow('claude missing from PATH')
expect(queryMock).not.toHaveBeenCalled()
const run = await ctx.subagents.start('claude-code', request())
child.settle({ exitCode: 9, signal: null })
child.stdout.end()
@@ -362,6 +370,13 @@ describe('task admission and package contracts', () => {
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
'subagent-claude-code: child run failed (error):',
))
expect(resolveExecutable).toHaveBeenCalledWith(
'claude',
expect.objectContaining({ ANTHROPIC_API_KEY: 'provider-fake-key' }),
expect.any(AbortSignal),
)
expect(queryMock.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable)
.toBe('/native/claude')
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
cwd: process.cwd(),
graceMs: 29,
@@ -441,6 +456,19 @@ describe('official spawn projection', () => {
)).toThrow('SDK spawn request omitted its workspace')
})
it.each(['cmd', 'bat'])('routes a Windows .%s shim through cmd.exe', (extension) => {
const command = String.raw`C:\Program Files\Claude\claude.${extension}`
const spec = claudeSpawnSpec(sdkSpawnOptions({
command,
args: ['--output-format', 'stream-json'],
}), 7, 'win32')
expect(spec.argv).toEqual([
'cmd.exe', '/d', '/s', '/c', command,
'--output-format', 'stream-json',
])
})
it('projects streams, exit facts, listeners, and idempotent tree termination', async () => {
const child = fakeChild({ exitOnTerminate: false })
const process = new ManagedClaudeCodeProcess(child.handle)
@@ -508,6 +536,7 @@ describe('query options and result mapping', () => {
const captured: SubprocessHandle[] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: {
HOST_VISIBLE: 'overridden',
ANTHROPIC_API_KEY: 'explicit-fake-key',
@@ -523,6 +552,7 @@ describe('query options and result mapping', () => {
expect(options).toMatchObject({
abortController: controller,
cwd: '/workspace',
pathToClaudeCodeExecutable: '/native/claude',
persistSession: false,
disallowedTools: ['AskUserQuestion'],
})
@@ -670,6 +700,7 @@ describe('run publication, cancellation, and settlement', () => {
let index = 0
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => children[index++]!.handle,
@@ -720,6 +751,7 @@ describe('run publication, cancellation, and settlement', () => {
request(undefined, parentAbort.signal),
{
cwd: '/workspace',
executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => child.handle,

View File

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

View File

@@ -27,7 +27,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. The plugin does not install Codex, select a model, create `CODEX_HOME`, log in, or probe a version. Credential-shaped ambient variables are removed by the subprocess seam, so an API key intended for the child must be supplied explicitly in `env`; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
Install this package and add the following rows to your own `cordis.yml`. Shipped CLI configurations do not load this provider or expose `subagent_codex` by default.
Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. A custom host composition can still use both rows directly.
```yaml
- id: subagent-codex
@@ -38,6 +38,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: codex
toolName: subagent_codex

View File

@@ -27,7 +27,7 @@
生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH``HOME` 等普通环境变量值仍然可用。
请安装此包,并将以下配置项添加到你自己的 `cordis.yml`。正式 CLI 配置默认不会加载此提供方,也不会暴露 `subagent_codex`
随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`自定义宿主组装仍可直接使用两条配置行。
```yaml
- id: subagent-codex
@@ -38,6 +38,7 @@
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
disabled: true
config:
provider: codex
toolName: subagent_codex