Merge pull request #2392 from deepseek-harness/codex/installable-product-subagents

feat(subagent): make Claude Code provider directly installable
This commit is contained in:
pku-xht
2026-08-18 19:33:03 +08:00
committed by GitHub
55 changed files with 699 additions and 280 deletions
+11 -5
View File
@@ -87,6 +87,11 @@ export interface PackageManifest {
devDependencies?: Record<string, string>
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
dsh?: {
bundle?: {
patch?: string
}
}
}
/** One workspace manifest and its repo-relative path. */
@@ -134,10 +139,6 @@ function workspaceManifests(): WorkspaceManifest[] {
}
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
// Profile bundles publish their dsh.bundle.patch layer beside the lib.
'@deepseek-ai/dsh-base': ['cordis.patch.yml'],
'@deepseek-ai/dsh-web-app': ['cordis.patch.yml'],
'@deepseek-ai/dsh-headless': ['cordis.patch.yml'],
// Statically linked client libraries keep their stylesheets next to the emitted
// JavaScript, which imports them by relative path: the compile shell runs
// them through its own CSS pipeline, so the sheets are published artifacts.
@@ -164,7 +165,12 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
}
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
const declaredPatch = manifest.dsh?.bundle?.patch
const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
const extras = [
...bundleFiles,
...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
]
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
@@ -14,7 +14,7 @@ describe('configuration source ownership gate', () => {
it('rejects inline endpoints in shipped bundle patches', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-config-source-ownership-'))
roots.push(root)
const directory = join(root, 'packages/bundle/base')
const directory = join(root, 'packages/subagent/subagent-claude-code')
mkdirSync(directory, { recursive: true })
writeFileSync(
join(directory, 'cordis.patch.yml'),
@@ -22,7 +22,7 @@ describe('configuration source ownership gate', () => {
)
expect(collectConfigSourceOwnershipViolations(root)).toEqual([
'packages/bundle/base/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
'packages/subagent/subagent-claude-code/cordis.patch.yml:2: inlines a credential or endpoint from the environment.'
+ ' The adapter resolves apiKeyEnv through ctx.credentials and the endpoint through the'
+ ' environment snapshot; inlining here bypasses both ladders.',
])
+2 -1
View File
@@ -14,7 +14,8 @@ const SHIPPED_CONFIG_GLOBS = [
'apps/*/config/*.yml',
'examples/*/*.cordis.yml',
'examples/*/cordis.yml',
'packages/bundle/*/cordis.patch.yml',
// Bundle identity comes from the package manifest, not the domain directory.
'packages/*/*/cordis.patch.yml',
// The Python runtime ships its own default composition inside the wheel.
'python/*/src/**/cordis.yml',
]
+50 -1
View File
@@ -4,8 +4,15 @@
* metadata field must stay static, and a disabled expression must parse.
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { metadataExpressionErrors } from './verify-cordis-config.ts'
import {
bundleManifestPaths,
bundlePluginDependencyErrors,
metadataExpressionErrors,
} from './verify-cordis-config.ts'
describe('verify-cordis-config metadata expressions', () => {
it('accepts a disabled !!js expression', () => {
@@ -37,3 +44,45 @@ describe('verify-cordis-config metadata expressions', () => {
expect(problems.some(problem => problem.includes('[0].disabled: disabled expression does not parse'))).toBe(true)
})
})
describe('workspace Bundle discovery and product dependency closures', () => {
it('discovers a Bundle outside packages/bundle from its manifest declaration', () => {
const fixture = mkdtempSync(join(tmpdir(), 'dsh-bundle-discovery-'))
try {
const bundleDir = join(fixture, 'packages/subagent/example')
const plainDir = join(fixture, 'packages/bundle/plain')
mkdirSync(bundleDir, { recursive: true })
mkdirSync(plainDir, { recursive: true })
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify({
name: '@deepseek-ai/dsh-subagent-example',
dsh: { bundle: { patch: './cordis.patch.yml' } },
}))
writeFileSync(join(plainDir, 'package.json'), JSON.stringify({
name: '@deepseek-ai/dsh-plain',
}))
expect(bundleManifestPaths(fixture)).toEqual([
'packages/subagent/example/package.json',
])
} finally {
rmSync(fixture, { recursive: true, force: true })
}
})
it('allows a Bundle to mount itself but rejects an undeclared plugin package', () => {
const manifestPath = 'packages/subagent/example/package.json'
const file = 'packages/subagent/example/cordis.patch.yml'
const manifest = {
name: '@deepseek-ai/dsh-subagent-example',
dependencies: {},
}
const self = { file, name: '@deepseek-ai/dsh-subagent-example' }
expect(bundlePluginDependencyErrors(manifestPath, manifest, [self])).toEqual([])
expect(bundlePluginDependencyErrors(manifestPath, manifest, [
self,
{ file, name: '@deepseek-ai/dsh-missing-plugin' },
])).toEqual([
`${file}: @deepseek-ai/dsh-missing-plugin must be declared in ${manifestPath} dependencies`,
])
})
})
+47 -13
View File
@@ -20,12 +20,14 @@ interface JsExpr {
__jsExpr: string
}
interface PackageManifest {
export interface PackageManifest {
name?: string
dependencies?: Record<string, string>
optionalDependencies?: Record<string, string>
dsh?: { bundle?: { patch?: string } }
}
interface PluginReference {
export interface PluginReference {
file: string
name: string
}
@@ -260,11 +262,14 @@ function validateExampleResolution(): string[] {
function validateAppResolution(): string[] {
const violations: string[] = []
const bundleManifests = bundleManifestPaths()
// App overlays (and any config left under apps/cli/config) resolve from the
// dsh app's own dependency surface — the profile module fallback mirrors it.
const appDependencies = {
...readManifest('apps/cli/package.json').dependencies,
// The fallback also links every bundle's own dependencies (healProfilesModuleFallback).
// The fallback also links every in-box bundle's own dependencies
// (healProfilesModuleFallback). Optional Profile bundles stay outside the
// app installation until that Profile installs them.
...Object.fromEntries(globSync('packages/bundle/*/package.json', { cwd: root })
.flatMap(file => Object.entries(readManifest(file).dependencies ?? {}))),
}
@@ -274,20 +279,49 @@ function validateAppResolution(): string[] {
violations.push(...missingPluginDependencies(appReferences, appDependencies, 'apps/cli/package.json or a bundle manifest'))
// Each bundle's patch rows must resolve from that bundle's own dependencies:
// per-layer resolution anchors on the bundle package directory.
for (const manifestPath of globSync('packages/bundle/*/package.json', { cwd: root })) {
for (const manifestPath of bundleManifests) {
const bundleDir = manifestPath.replace(/\/package\.json$/, '')
const manifest = readManifest(manifestPath)
const references = pluginReferences.filter(reference => reference.file.startsWith(`${bundleDir}/`))
violations.push(...missingPluginDependencies(
// A bundle may mount its own package (the web-app runtime row).
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
manifest.dependencies ?? {},
manifestPath,
))
const patch = manifest.dsh?.bundle?.patch
if (typeof patch !== 'string') continue
const patchFile = relative(root, resolve(root, bundleDir, patch)).replaceAll('\\', '/')
const references = pluginReferences.filter(reference => reference.file === patchFile)
violations.push(...bundlePluginDependencyErrors(manifestPath, manifest, references))
}
return violations
}
/**
* Discover workspace Bundle packages from their manifest declaration.
* @param repoRoot Repository root to scan.
* @returns Sorted repository-relative package manifest paths.
*/
export function bundleManifestPaths(repoRoot: string = root): string[] {
return globSync('packages/*/*/package.json', { cwd: repoRoot })
.filter(path => typeof readManifest(path, repoRoot).dsh?.bundle?.patch === 'string')
.sort()
}
/**
* Validate plugin packages referenced by one Bundle patch.
* @param manifestPath Repository-relative Bundle manifest path.
* @param manifest Parsed Bundle manifest.
* @param references Plugin rows read from the Bundle package directory.
* @returns Missing production dependency diagnostics.
*/
export function bundlePluginDependencyErrors(
manifestPath: string,
manifest: PackageManifest,
references: readonly PluginReference[],
): string[] {
return missingPluginDependencies(
// A Bundle may mount its own package (for example, its provider or runtime row).
references.filter(reference => packageNameFromSpecifier(reference.name) !== manifest.name),
manifest.dependencies ?? {},
manifestPath,
)
}
/**
* Every configured specifier of a local workspace package must resolve through
* the tsconfig `paths` facade to a `.ts`/`.tsx` source file. The `dsh` source
@@ -363,8 +397,8 @@ function missingPluginDependencies(
: `${[...locations].join(', ')}: ${packageName} must be declared in ${manifestPath} dependencies`)
}
function readManifest(path: string): PackageManifest {
return JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest
function readManifest(path: string, repoRoot: string = root): PackageManifest {
return JSON.parse(readFileSync(resolve(repoRoot, path), 'utf8')) as PackageManifest
}
function localPackageDirectories(): Map<string, string> {