mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(subagent-claude-code): tighten provider evidence
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
|
||||
2026-06-21-subagent-capability-seam.md: 35fe7b7aaf02d9d55012e3285b3f5a58bc76cde8
|
||||
2026-06-21-subagent-capability-seam.zh.md: 221335859cec104a55136201e4923d783d616e86
|
||||
2026-06-21-subagent-capability-seam.md: 752e639b09ea2ac0ba19841ddfe38b15b44d22e9
|
||||
2026-06-21-subagent-capability-seam.zh.md: 6009aeda6773357a4217f8956fb510c2116bbe3f
|
||||
|
||||
@@ -54,7 +54,7 @@ Fresh and forked children are separate providers, not a request flag. `dsh-subag
|
||||
|
||||
### Child isolation and the parent log
|
||||
|
||||
Each subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. The parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output) — the child's internal steps and tool calls stay in the child's own session, never injected into the parent log. This is the only design that is identical across transports: an ACP child's internal events physically cannot be injected into our parent log, so making in-process behave the same keeps the seam transport-agnostic.
|
||||
Each in-process subagent runs in its **own `Session`** (own id, `parentSession` lineage), persisted independently. Remote ACP and one-shot product providers instead mint a parent-scoped lifecycle id and expose no local `Agent` or child `Session`; their internal state remains in the remote process. Across both forms, the parent's log records only the spawn `tool/call` and its `tool/result` (the child's final output), while child steps and tool calls remain outside the parent log.
|
||||
|
||||
### Synchronous collect (first cut)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ bash seam([能力 seam](../architecture/2026-06-13-capability-seams.md))在
|
||||
|
||||
### 子 agent 隔离与父日志
|
||||
|
||||
每个 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。父日志仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出)——子 agent 的内部步骤和工具调用留在子 agent 自己的会话中,绝不注入父日志。这是唯一在所有传输方式下行为一致的设计:ACP 子 agent 的内部事件在物理上无法注入我们的父日志,因此让进程内行为保持一致,使 seam 真正与传输方式无关。
|
||||
每个进程内 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。远端 ACP 和一次性产品提供方则会生成一个父级作用域的生命周期 id,且不暴露本地 `Agent` 或子 `Session`;其内部状态留在远端进程中。两种形式下,父日志都仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出),而子 agent 的步骤和工具调用均留在父日志之外。
|
||||
|
||||
### 同步收集(首版)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by `scripts/gen-third-party-notices.ts`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and `scripts/gen-third-party-notices.spec.ts` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run `pnpm run verify-third-party-notices` for the standalone check.
|
||||
|
||||
|
||||
@@ -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: 5bfc8ff3ba539b6caf891577cee5820d983106cd
|
||||
README.zh.md: 6db9148228ba30c0f84beb14caa7a84aa0dcd9f5
|
||||
README.md: e62f60fceea16749296a91377785b81d94d751ca
|
||||
README.zh.md: e171524157b2b1696df31753816210d41637a911
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 endpoint intended for the child must be supplied there; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 密钥或端点必须在该配置中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量仍然可用。
|
||||
生产环境使用 `@anthropic-ai/claude-agent-sdk` 提供的 Claude Code CLI,以及宿主机原生设置与身份验证。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。
|
||||
|
||||
请安装此包,并将以下配置项添加到你自己的 `cordis.yml`。正式 CLI 配置默认不会加载此提供方,也不会暴露 `subagent_claude_code`。
|
||||
|
||||
|
||||
@@ -60,11 +60,17 @@ class ClaudeCodeProvider implements SubagentProvider {
|
||||
) {}
|
||||
|
||||
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 spec: ClaudeCodeRunSpec = {
|
||||
cwd: resolveChildCwd(
|
||||
'subagent-claude-code',
|
||||
undefined,
|
||||
request.parent.session.header.cwd,
|
||||
parentCwd,
|
||||
),
|
||||
env: this.config.env,
|
||||
disposeGraceMs: this.config.disposeGraceMs,
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function startMessagesFixture(
|
||||
request.on('data', (chunk: Buffer) => { chunks.push(chunk) })
|
||||
request.on('end', () => {
|
||||
const path = request.url ?? ''
|
||||
if (!path.startsWith('/v1/messages')) {
|
||||
if (path !== '/v1/messages' && !path.startsWith('/v1/messages?')) {
|
||||
response.writeHead(404, { 'content-type': 'application/json' })
|
||||
response.end(JSON.stringify({
|
||||
type: 'error',
|
||||
|
||||
@@ -10,6 +10,11 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import type {
|
||||
Query,
|
||||
SDKMessage,
|
||||
SDKSystemMessage,
|
||||
} from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Context } from 'cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
@@ -23,6 +28,39 @@ import {
|
||||
type MessagesFixture,
|
||||
} from './messages-fixture.ts'
|
||||
|
||||
const observedSdkMessages = vi.hoisted((): SDKMessage[] => [])
|
||||
|
||||
vi.mock('@anthropic-ai/claude-agent-sdk', async (importOriginal) => {
|
||||
const actual = await importOriginal<
|
||||
typeof import('@anthropic-ai/claude-agent-sdk')
|
||||
>()
|
||||
return {
|
||||
...actual,
|
||||
query(options: Parameters<typeof actual.query>[0]): Query {
|
||||
const query = actual.query(options)
|
||||
// Observe the real SDK stream without replacing its protocol or CLI.
|
||||
return new Proxy(query, {
|
||||
get(target, property) {
|
||||
if (property === Symbol.asyncIterator) {
|
||||
return async function* (): AsyncGenerator<SDKMessage, void> {
|
||||
for await (const message of target) {
|
||||
observedSdkMessages.push(message)
|
||||
yield message
|
||||
}
|
||||
}
|
||||
}
|
||||
const value: unknown = Reflect.get(target, property, target)
|
||||
if (typeof value === 'function') {
|
||||
const method = value as (...args: unknown[]) => unknown
|
||||
return method.bind(target)
|
||||
}
|
||||
return value
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const sdkRoot = dirname(fileURLToPath(
|
||||
import.meta.resolve('@anthropic-ai/claude-agent-sdk'),
|
||||
@@ -54,6 +92,7 @@ afterEach(async () => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
observedSdkMessages.length = 0
|
||||
})
|
||||
|
||||
interface RealHarness {
|
||||
@@ -168,10 +207,16 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
|
||||
})
|
||||
await run.dispose()
|
||||
|
||||
const initMessage = observedSdkMessages.find(
|
||||
(message): message is SDKSystemMessage =>
|
||||
message.type === 'system' && message.subtype === 'init',
|
||||
)
|
||||
expect(initMessage?.claude_code_version).toBe('2.1.220')
|
||||
|
||||
expect(fixture.requests).toHaveLength(1)
|
||||
const recorded = fixture.requests[0]!
|
||||
expect(recorded.method).toBe('POST')
|
||||
expect(recorded.path).toMatch(/^\/v1\/messages(?:\\?|$)/)
|
||||
expect(recorded.path).toMatch(/^\/v1\/messages(?:\?.*)?$/)
|
||||
expect(recorded.headers['x-api-key']).toBe(fakeKey)
|
||||
expect(recorded.body.model).toBe(settingsModel)
|
||||
expect(Array.isArray(recorded.body.messages)).toBe(true)
|
||||
|
||||
@@ -341,6 +341,17 @@ describe('task admission and package contracts', () => {
|
||||
disposeGraceMs: 29,
|
||||
})
|
||||
|
||||
await expect(ctx.subagents.start('claude-code', {
|
||||
...request(),
|
||||
parent: {
|
||||
id: 'parent-without-cwd',
|
||||
session: { header: {} },
|
||||
} as unknown as Agent,
|
||||
})).rejects.toThrow(
|
||||
'subagent-claude-code: no working directory for the child — delegate from a parent session that has one',
|
||||
)
|
||||
expect(queryMock).not.toHaveBeenCalled()
|
||||
|
||||
const run = await ctx.subagents.start('claude-code', request())
|
||||
child.settle({ exitCode: 9, signal: null })
|
||||
child.stdout.end()
|
||||
|
||||
@@ -25,7 +25,9 @@ describe('THIRD_PARTY_NOTICES.md', () => {
|
||||
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
|
||||
// this assertion means the notices were committed without that hook.
|
||||
it('matches what the generator produces from the current manifests', () => {
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(render())
|
||||
const generated = render()
|
||||
expect(generated).toContain('It depends on the third-party software listed below.')
|
||||
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -694,7 +694,7 @@ export function render(): string {
|
||||
|
||||
# Third-Party Notices
|
||||
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party open-source software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
DeepSeek Harness is licensed under [BSD 3-Clause](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
|
||||
|
||||
This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user