mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge pull request #2392 from deepseek-harness/codex/installable-product-subagents
feat(subagent): make Claude Code provider directly installable
This commit is contained in:
@@ -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.',
|
||||
])
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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`,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user