Merge commit 'a9cc0fddb40be295c43cb2badb4cbcb2b032556c' into codex/product-providers-pr2-claude-code

# Conflicts:
#	.agents/notes/proposed/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml
#	.agents/notes/proposed/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
#	.agents/notes/proposed/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md
#	docs/core-data-structures/subprocess.i18n.yaml
#	packages/subprocess/subprocess/README.i18n.yaml
This commit is contained in:
pku-xht
2026-08-05 04:41:27 +08:00
34 changed files with 140 additions and 171 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/subagent/subagent-claude-code/README.md
README.md: 59d7dcd8406cb244854bbb704f644af1a6aa8589
README.zh.md: 90ea0a316b91da3249b934984a3f512b71b8b3b2
README.md: 5bfc8ff3ba539b6caf891577cee5820d983106cd
README.zh.md: 6db9148228ba30c0f84beb14caa7a84aa0dcd9f5

View File

@@ -27,7 +27,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
| Key | Default | Meaning |
|---|---|---|
| `env` | `{}` | Explicit SDK/CLI environment layered over the shared credential-scrubbed parent environment. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
| `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 endpoint intended for the child must be supplied there; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
@@ -51,7 +51,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. The keyless real-product spec drives that official SDK and CLI against a loopback Anthropic Messages SSE service with a non-empty fake key. It proves the exact task, authentication, byte-exact answer, inherited host-setting marker, process failure, cancellation, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration alongside the Codex provider with neither product command available, verifies both fixed foreground-only tools, and records zero child starts. A credentialed e2e maps a runtime-only DeepSeek key to the official Claude Code environment contract, starts the production provider and real SDK/CLI against the fixed official `/anthropic` endpoint, requires a unique nonce, and proves process-tree exit without persisting the key or calling the Messages API directly from the test.
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 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.

View File

@@ -27,7 +27,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
| 配置键 | 默认值 | 含义 |
|---|---|---|
| `env` | `{}` | 显式指定的 SDK/CLI 环境,叠加在由共享机制清除凭证后的父环境之上。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值;随后资源释放会等待整棵进程树退出。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md);随后资源释放会等待整棵进程树退出。 |
生产环境使用 `@anthropic-ai/claude-agent-sdk` 提供的 Claude Code CLI以及宿主机原生设置与身份验证。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或端点必须在该配置中显式提供;除非被覆盖,`PATH``HOME` 等普通环境变量仍然可用。
@@ -51,7 +51,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK
## 产品兼容性与证据
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其平台可选依赖提供 Claude Code 2.1.220。无密钥真实产品测试使用非空伪密钥,驱动该官方 SDK 与 CLI 连接回环 Anthropic Messages SSE 服务,并证明原始任务、身份验证、逐字节一致的答案、继承的宿主设置标记、进程失败、取消与进程树退出。独立的 Loader 装配 e2e 会在两个产品命令均不可用时,与 Codex 提供方一起启动和 README 所示形态相同的用户配置,验证两个固定且只支持前台执行的工具,并记录零次子级启动。带密钥 e2e 会将仅在运行时提供的 DeepSeek 密钥映射到 Claude Code 官方环境契约,启动生产提供方与真实 SDK 和 CLI 并连接固定的官方 `/anthropic` 端点,要求获得一个唯一随机数,并在不持久化密钥且不由测试直接调用 Messages API 的情况下证明进程树退出
运行时依赖精确锁定为 `@anthropic-ai/claude-agent-sdk@0.3.220`,其平台可选依赖提供 Claude Code 2.1.220。强制证据会通过无密钥回环产品路径与带密钥 DeepSeek 路径运行该官方发行版,而 Loader 组合则证明两个选择启用的产品包能够共存,且不会启动任一产品
项目所有者按身份范围授权分发官方 SDK 及每个 SDK 版本声明的官方 CLI平台载荷。[`THIRD_PARTY_NOTICES.md`](../../../THIRD_PARTY_NOTICES.md) 会披露当前可选载荷闭包,但不会把其声明条款归类为宽松许可证;其他无关的非宽松运行时依赖仍会使第三方声明门禁失败。

View File

@@ -32,6 +32,7 @@
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-subagent": "^0.0.1",
"@deepseek-ai/dsh-subprocess": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
@@ -48,6 +49,7 @@
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -8,6 +8,7 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import {
assertPositiveFinite,
NO_START_CAPABILITIES,
@@ -90,6 +91,11 @@ export function apply(ctx: Context, config: Config): void {
'disposeGraceMs',
resolved.disposeGraceMs,
)
if (resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) {
throw new Error(
`subagent-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`,
)
}
ctx.subagents.registerProvider(new ClaudeCodeProvider(ctx, resolved))
}
/* jscpd:ignore-end */

View File

@@ -83,18 +83,17 @@ function complete(
/**
* Start a loopback-only Anthropic Messages SSE fixture.
* @param script - one behavior per Messages request.
* @param behavior - the single response behavior for this fixture.
* @returns the bound server and its recorded requests.
*/
export async function startMessagesFixture(
script: readonly MessagesBehavior[],
behavior: MessagesBehavior,
): Promise<MessagesFixture> {
const requests: RecordedMessagesRequest[] = []
let requestStartedResolve!: () => void
const requestStarted = new Promise<void>((resolve) => {
requestStartedResolve = resolve
})
let behaviorIndex = 0
const server = createServer((request, response) => {
const chunks: Buffer[] = []
request.on('data', (chunk: Buffer) => { chunks.push(chunk) })
@@ -117,18 +116,6 @@ export async function startMessagesFixture(
body,
})
requestStartedResolve()
const behavior = script[behaviorIndex++]
if (behavior === undefined) {
response.writeHead(500, { 'content-type': 'application/json' })
response.end(JSON.stringify({
type: 'error',
error: {
type: 'api_error',
message: 'Messages fixture script was exhausted',
},
}))
return
}
if (behavior.kind === 'complete') {
complete(response, body, behavior.text)
}

View File

@@ -64,7 +64,7 @@ interface RealHarness {
readonly env: Record<string, string>
}
async function realHarness(script: readonly MessagesBehavior[]): Promise<{
async function realHarness(behavior: MessagesBehavior): Promise<{
readonly harness: RealHarness
readonly fixture: MessagesFixture
}> {
@@ -80,7 +80,7 @@ async function realHarness(script: readonly MessagesBehavior[]): Promise<{
join(claudeConfig, 'settings.json'),
`${JSON.stringify({ model: settingsModel }, null, 2)}\n`,
)
const fixture = await startMessagesFixture(script)
const fixture = await startMessagesFixture(behavior)
fixtures.push(fixture)
const env = {
ANTHROPIC_API_KEY: fakeKey,
@@ -149,9 +149,10 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
it('inherits host settings and sends the exact task and fake key to local Messages', async () => {
const sentinel = 'REAL_CLAUDE_CODE_SENTINEL_2_1_220'
const task = 'Return the fixture sentinel exactly.'
const { harness, fixture } = await realHarness([
{ kind: 'complete', text: sentinel },
])
const { harness, fixture } = await realHarness({
kind: 'complete',
text: sentinel,
})
expect(sdkPackage.version).toBe('0.3.220')
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
@@ -191,7 +192,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
})
it('maps a real CLI process failure to error', async () => {
const { harness, fixture } = await realHarness([{ kind: 'hold' }])
const { harness, fixture } = await realHarness({ kind: 'hold' })
const run = await startRequest(harness, 'Exercise the failure path.')
await fixture.requestStarted
expect(harness.handles).toHaveLength(1)
@@ -207,7 +208,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
})
it('settles cancellation and leaves the real SDK-spawned CLI tree quiescent', async () => {
const { harness, fixture } = await realHarness([{ kind: 'hold' }])
const { harness, fixture } = await realHarness({ kind: 'hold' })
const controller = new AbortController()
const run = await startRequest(
harness,

View File

@@ -27,6 +27,7 @@ import type {
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import * as claudeCode from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
@@ -74,8 +75,6 @@ async function nextTask(): Promise<void> {
interface FakeChildOptions {
readonly pid?: number
readonly stdin?: PassThrough | undefined
readonly stdout?: PassThrough | undefined
readonly exitOnTerminate?: boolean
readonly waitForExitError?: Error
readonly doneError?: Error
@@ -145,8 +144,8 @@ function fakeChild(options: FakeChildOptions = {}): FakeChild {
})
const handle: SubprocessHandle = {
pid: options.pid ?? 1234,
stdin: options.stdin === undefined ? stdin : options.stdin,
stdout: options.stdout === undefined ? stdout : options.stdout,
stdin,
stdout,
stderr: undefined,
collected: {},
done,
@@ -232,7 +231,6 @@ function sdkSpawnOptions(
interface FakeRun {
readonly child: FakeChild
readonly query: Query
readonly close: ReturnType<typeof vi.fn>
readonly spawnSpecs: SubprocessSpawnSpec[]
readonly options: Options[]
@@ -262,7 +260,7 @@ function fakeRun(
params.options.spawnClaudeCodeProcess!(sdkSpawnOptions())
return query
})
return { child, query, close, spawnSpecs, options, spec }
return { child, close, spawnSpecs, options, spec }
}
beforeEach(() => {
@@ -318,6 +316,11 @@ describe('task admission and package contracts', () => {
await expect(ctx.plugin(claudeCode, { disposeGraceMs }))
.rejects.toThrow('disposeGraceMs must be a positive finite number')
}
await expect(ctx.plugin(claudeCode, {
disposeGraceMs: MAX_TIMER_DELAY_MS + 1,
})).rejects.toThrow(
`disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`,
)
await ctx.fiber.dispose()
})

View File

@@ -27,6 +27,9 @@
{
"path": "../../subprocess/subprocess"
},
{
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
}