mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-13 04:03:30 +00:00
Merge commit '0c177e17a23692d018a79bae59ac9a2db6eba59c' into codex/subprocess-native-containment
# Conflicts: # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/input.json # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/bundle/acp-app/package.json # packages/examples/jsonrpc-demo/src/packaged-bin.ts # packages/subagent/subagent-acp/src/run.ts # packages/subagent/subagent-acp/tests/subagent-acp.spec.ts # packages/subprocess/subprocess-local/README.i18n.yaml # packages/subprocess/subprocess-local/README.md # packages/subprocess/subprocess-local/README.zh.md # pnpm-lock.yaml # scripts/smoke-python-runtime.py
This commit is contained in:
+31
-24
@@ -11,6 +11,8 @@ import { fileURLToPath } from 'node:url'
|
||||
import { loadLayeredEnv } from '@deepseek-ai/dsh-app-boot'
|
||||
import { parseDshArgs } from './args.ts'
|
||||
|
||||
const PACKAGED_RUNNER_ARG = '--dsh-internal-subprocess-runner'
|
||||
|
||||
// Both the source tree (apps/cli/src) and the bundled bin (apps/cli/lib) sit
|
||||
// one directory under apps/cli, so the checked-in manifest resolves with the
|
||||
// same relative hop from either artifact.
|
||||
@@ -21,30 +23,35 @@ function readVersion(): string {
|
||||
return typeof manifest.version === 'string' ? manifest.version : '0.0.0'
|
||||
}
|
||||
|
||||
const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
if (process.argv[2] === PACKAGED_RUNNER_ARG) {
|
||||
process.argv.splice(2, 1)
|
||||
await import('@deepseek-ai/dsh-subprocess-local/spawn-runner')
|
||||
} else {
|
||||
const invocation = parseDshArgs(process.argv.slice(2), readVersion())
|
||||
|
||||
switch (invocation.mode) {
|
||||
case 'profile': {
|
||||
const { runProfile } = await import('./profile-boot.ts')
|
||||
await runProfile({
|
||||
environment: loadLayeredEnv('dsh'),
|
||||
profile: invocation.profile,
|
||||
patchFiles: invocation.patches,
|
||||
args: invocation.args,
|
||||
})
|
||||
break
|
||||
switch (invocation.mode) {
|
||||
case 'profile': {
|
||||
const { runProfile } = await import('./profile-boot.ts')
|
||||
await runProfile({
|
||||
environment: loadLayeredEnv('dsh'),
|
||||
profile: invocation.profile,
|
||||
patchFiles: invocation.patches,
|
||||
args: invocation.args,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'plugin': {
|
||||
const { runPlugin } = await import('./plugin.ts')
|
||||
process.exit(runPlugin(invocation.profile, invocation.args))
|
||||
break
|
||||
}
|
||||
case 'dump-config': {
|
||||
const { runDumpConfig } = await import('./dump-config.ts')
|
||||
runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
|
||||
break
|
||||
}
|
||||
default:
|
||||
invocation satisfies never
|
||||
throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`)
|
||||
}
|
||||
case 'plugin': {
|
||||
const { runPlugin } = await import('./plugin.ts')
|
||||
process.exit(runPlugin(invocation.profile, invocation.args))
|
||||
break
|
||||
}
|
||||
case 'dump-config': {
|
||||
const { runDumpConfig } = await import('./dump-config.ts')
|
||||
runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
|
||||
break
|
||||
}
|
||||
default:
|
||||
invocation satisfies never
|
||||
throw new Error(`dsh: unhandled invocation mode ${JSON.stringify(invocation)}`)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,12 @@ function anchorPathSpec(argument: string, cwd: string): string {
|
||||
export function runPlugin(profile: string, args: readonly string[]): number {
|
||||
const dir = resolveProfileDir(profile)
|
||||
if (!existsSync(join(dir, 'package.json'))) {
|
||||
initProfile(dir, PROFILE_TEMPLATES[profile] ?? DEFAULT_PROFILE_BUNDLES)
|
||||
const template = PROFILE_TEMPLATES[profile]
|
||||
initProfile(
|
||||
dir,
|
||||
template?.bundles ?? DEFAULT_PROFILE_BUNDLES,
|
||||
template?.patchReload,
|
||||
)
|
||||
process.stderr.write(`${NAME}: initialized profile ${profile} at ${dir}\n`)
|
||||
}
|
||||
const before = readProfileManifest(NAME, dir)
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Shared profile boot for every `dsh` surface: resolve the profile, stack its
|
||||
* patch layers (bundle layers in `dsh.profile.bundles` order, the profile's
|
||||
* own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
|
||||
* tree over the profile's empty root config, keep the profile patch layer
|
||||
* live, and wire fail-loud plus bounded shutdown.
|
||||
* tree over the profile's empty root config, apply its selected patch-reload
|
||||
* lifecycle, and wire fail-loud plus bounded shutdown.
|
||||
*
|
||||
* App flags are not the launcher's business: the invocation's inner arguments
|
||||
* are provided to the tree through `ctx.cmdlineArgs`, where any injected app
|
||||
@@ -30,16 +30,36 @@ import {
|
||||
type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
|
||||
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
|
||||
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
|
||||
|
||||
import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { provideCmdline, type AppReady } from '@deepseek-ai/dsh-cmdline'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
/** Launcher-owned readiness signal committed only after boot and host setup succeed. */
|
||||
function createAppReady(): { service: AppReady; commit(): void } {
|
||||
let ready = false
|
||||
const listeners = new Set<() => void>()
|
||||
return {
|
||||
service: {
|
||||
onReady(listener) {
|
||||
if (ready) {
|
||||
listener()
|
||||
return () => {}
|
||||
}
|
||||
listeners.add(listener)
|
||||
return () => { listeners.delete(listener) }
|
||||
},
|
||||
},
|
||||
commit() {
|
||||
if (ready) return
|
||||
ready = true
|
||||
for (const listener of [...listeners]) listener()
|
||||
listeners.clear()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The home-level user patch layer (`$DSH_HOME/cordis.patch.yml`), applied
|
||||
* over every profile's own layer. Resolved per call, not at module load:
|
||||
@@ -83,26 +103,25 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a resolved profile for `name`: heal the shared module fallback, then
|
||||
* (re)write the empty root config. The root is always rewritten: the whole
|
||||
* composition is patch layers, and the vendored Loader's tree write-back (a
|
||||
* plugin self-disposing persists the current tree) can bake composed rows
|
||||
* into this file — which would duplicate every bundle insert on the next
|
||||
* boot. The file exists on disk only because the Loader needs a real include
|
||||
* root to anchor `baseUrl` at the profile directory (the config dump anchors
|
||||
* on the same file, so both compose over the identical base).
|
||||
* Load a resolved profile for `name` and (re)write the empty root config. The
|
||||
* root is always rewritten: the whole composition is patch layers, and the
|
||||
* vendored Loader's tree write-back (a plugin self-disposing persists the
|
||||
* current tree) can bake composed rows into this file — which would duplicate
|
||||
* every bundle insert on the next boot. The file exists on disk only because
|
||||
* the Loader needs a real include root to anchor `baseUrl` at the profile
|
||||
* directory (the config dump anchors on the same file, so both compose over
|
||||
* the identical base).
|
||||
* @param name - the profile name.
|
||||
* @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
|
||||
* @returns the loaded profile.
|
||||
*/
|
||||
export function prepareProfile(name: string, userLayer = true): Profile {
|
||||
healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
|
||||
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
|
||||
return profile
|
||||
}
|
||||
|
||||
/** One profile's patch layers (application order) and the row index of its pre-flag composition. */
|
||||
/** One profile's patch layers, in application order. */
|
||||
interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle layers concatenated — the part below the user layers on a live reload. */
|
||||
@@ -111,11 +130,6 @@ interface ComposedProfile {
|
||||
homePatches: PatchOptions[]
|
||||
/** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
|
||||
overlays: PatchOptions[]
|
||||
/**
|
||||
* id → row of the composed tree (bundles + user layers + overlays), for the
|
||||
* launcher's own row checks.
|
||||
*/
|
||||
rows: ReadonlyMap<string, EntryOptions>
|
||||
}
|
||||
|
||||
/** The full patch stack of one composed profile, in application order. */
|
||||
@@ -130,19 +144,20 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
||||
* platform on its own rows), the profile's user layer, the home-level user
|
||||
* `dsh.profile.bundles` order (a base-backed profile gets the base bundle's
|
||||
* platform-gated shell rows), the profile's user layer, the home-level user
|
||||
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
||||
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
* @returns the profile, its patch layers, and the composed row index.
|
||||
* @returns the profile and its patch layers.
|
||||
*/
|
||||
function composeProfile(
|
||||
async function composeProfile(
|
||||
name: string,
|
||||
patchFiles: readonly string[],
|
||||
): ComposedProfile {
|
||||
): Promise<ComposedProfile> {
|
||||
await healProfilesModuleFallback(INSTALL_ANCHOR)
|
||||
const profile = prepareProfile(name)
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
@@ -152,22 +167,9 @@ function composeProfile(
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const composedOverlays = [...overlays]
|
||||
// The SHIPPED root is the part of the roster only this app can resolve: it
|
||||
// sits beside this app's own config, in both the source and built layouts.
|
||||
// The writable root the roster appends is `dsh-agent-presets`' own, so a
|
||||
// launcher that never reaches this patch still finds a person's presets.
|
||||
if (rows.has('agent-presets')) {
|
||||
composedOverlays.push({
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
...(rows.get('agent-presets')?.config ?? {}) as Record<string, unknown>,
|
||||
roots: [{ path: SHIPPED_PRESET_ROOT, trust: 'system' }],
|
||||
},
|
||||
})
|
||||
}
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
|
||||
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
|
||||
return { profile, bundlePatches, homePatches, overlays: composedOverlays }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -205,8 +207,9 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown
|
||||
* @returns the settled root context and the shutdown controller.
|
||||
*/
|
||||
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
|
||||
const composed = composeProfile(options.profile, options.patchFiles)
|
||||
const composed = await composeProfile(options.profile, options.patchFiles)
|
||||
const app: { current?: Context } = {}
|
||||
const appReady = createAppReady()
|
||||
const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })
|
||||
const signalShutdown = new AbortController()
|
||||
const interrupt = (code: number): void => {
|
||||
@@ -255,27 +258,26 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
provideCmdline(hostCtx, {
|
||||
args: options.args,
|
||||
exit: code => void shutdown.shutdown(code),
|
||||
ready: appReady.service,
|
||||
})
|
||||
})
|
||||
app.current = ctx
|
||||
// A surface can dispose the whole tree while boot or this post-boot watcher
|
||||
// setup is still in flight — a signal, or a fast one-shot's appExit. Loader
|
||||
// presence and fiber state own liveness; the initial check skips a tree
|
||||
// that already exited, and the catch below re-checks for an exit that
|
||||
// landed mid-setup. Watching is unconditional: a one-shot surface exits
|
||||
// through its bounded shutdown, which disposes the watchers before the
|
||||
// loop drains.
|
||||
if (!signalShutdown.signal.aborted
|
||||
// A live-reload profile can dispose the whole tree while post-boot watcher
|
||||
// setup is in flight — a signal or appExit. Loader presence and fiber state
|
||||
// own liveness; the initial check skips a tree that already exited, and the
|
||||
// catch below re-checks for an exit that landed mid-setup. Startup-frozen
|
||||
// profiles apply every user layer above but install no HMR fallback or watcher.
|
||||
if (composed.profile.patchReload === 'live'
|
||||
&& !signalShutdown.signal.aborted
|
||||
&& ctx.fiber.state === FiberState.ACTIVE
|
||||
&& ctx.get('loader') !== undefined) {
|
||||
try {
|
||||
// Config-only HMR for the live profile patch layer: the web bundle
|
||||
// disables the shared module-reload `hmr` row (its reload lifecycle is
|
||||
// untested), so when the composition leaves no HMR service, mount a
|
||||
// watch-only instance with no module roots — cordis.patch.yml edits stay
|
||||
// live on every long-lived surface. A silent skip would break the
|
||||
// documented hot-reload contract. HMR injects the timer service, which a
|
||||
// bare custom profile may not mount either.
|
||||
// Config-only HMR for the live profile patch layer: dsh-base disables
|
||||
// module reload by default, so when no profile explicitly enabled that
|
||||
// service, mount a watch-only instance with no module roots —
|
||||
// cordis.patch.yml edits stay live without replacing source modules. A
|
||||
// silent skip would break the documented reload contract. HMR injects
|
||||
// the timer service, which a bare custom profile may not mount either.
|
||||
if (ctx.get('hmr') === undefined) {
|
||||
if (ctx.get('timer') === undefined) {
|
||||
await ctx.loader.create({ name: '@deepseek-ai/cordis-plugin-timer' })
|
||||
@@ -296,5 +298,10 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
suppressShutdownError(ctx, signalShutdown.signal, error)
|
||||
}
|
||||
}
|
||||
if (!signalShutdown.signal.aborted
|
||||
&& ctx.fiber.state === FiberState.ACTIVE
|
||||
&& ctx.get('loader') !== undefined) {
|
||||
appReady.commit()
|
||||
}
|
||||
return { ctx, shutdown }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Clean source checkouts have no build-generated Typert contributor modules.
|
||||
# The SDK JSON-RPC application does not consume the Typert remote gateway;
|
||||
# installed builds retain the complete dsh-base row.
|
||||
- id: typert-loader
|
||||
disabled: true
|
||||
Reference in New Issue
Block a user