feat(subprocess): reshape the seam Node-ward for multi-consumer use
Review direction (tianyicui, PR #660): make the interface closer to Node's API so the other process-running places can adopt it. The spec gains per-stream stdio dispositions — 'pipe' (raw Readable/Writable for protocol streams), 'inherit' (diagnostics to the parent), and collect mode ({maxBytes, spill?} — the old bounded tail-keep shape, now with spill optional for diagnostic tails). SubprocessOutcome carries exit facts only; collected output stays readable through handle.collected after settlement (spill fds are sealed at the settle boundary). The handle grows Node-style kill(signal) (single signal, tree-scoped, no-op after settlement), terminate() (the SIGTERM→grace→SIGKILL escalation, also driven by the spec signal), waitForExit() (tree liveness, not just the direct child), and dispose() (the cooperative stdin-EOF→SIGTERM→SIGKILL ladder from subagent-subprocess, graces caller-supplied). Tree semantics are platform-correct: POSIX detached groups with direct-child fallback; Windows taskkill /T with an injectable runner. scrubbedParentEnv/SENSITIVE_ENV_PATTERN move to the seam as the one shared scrub definition. bash-local maps its config onto collect modes and batch stdin and reads results through the collected readers; its kill() maps to terminate() so task_kill keeps escalation semantics.
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
/**
|
||||
* The subprocess seam (`ctx.subprocess`): spawn fully-specified
|
||||
* commands into managed process groups with bounded, spill-backed output and
|
||||
* escalated kills. Command defaulting, shell semantics, deadlines, and
|
||||
* presentation belong to consumers — the bash executor seam is the owning
|
||||
* The subprocess seam (`ctx.subprocess`): spawn fully-specified commands into
|
||||
* managed process trees with Node-shaped stdio dispositions — raw pipes for
|
||||
* protocol streams, inherit for diagnostics, bounded spill-backed collection
|
||||
* for batch output — plus tree-scoped signalling and a cooperative dispose
|
||||
* ladder. Command defaulting, shell semantics, deadlines, framing, and
|
||||
* presentation belong to consumers; the bash executor seam is the owning
|
||||
* template. The local implementation lives in
|
||||
* `@deepseek-ai/dsh-subprocess-local`.
|
||||
* @module @deepseek-ai/dsh-subprocess
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import { DSH_ENV_PREFIX } from './types.ts'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
|
||||
|
||||
export { DSH_ENV_PREFIX } from './types.ts'
|
||||
@@ -16,13 +19,48 @@ export type {
|
||||
CollectedOutput,
|
||||
DshEnvironment,
|
||||
DshEnvironmentKey,
|
||||
SubprocessCollect,
|
||||
SubprocessCollectedOutputs,
|
||||
SubprocessDisposeGraces,
|
||||
SubprocessHandle,
|
||||
SubprocessOutcome,
|
||||
SubprocessOutputMode,
|
||||
SubprocessOutputRead,
|
||||
SubprocessOutputReader,
|
||||
SubprocessSpawnSpec,
|
||||
SubprocessStdinMode,
|
||||
SubprocessStdio,
|
||||
} from './types.ts'
|
||||
|
||||
/**
|
||||
* Credential-shaped environment names are NOT forwarded to children (the
|
||||
* harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a spawned
|
||||
* process implicitly). One heuristic for every in-repo spawner; a
|
||||
* deliberately supplied entry survives because explicit env layers merge
|
||||
* after the scrub.
|
||||
*/
|
||||
export const SENSITIVE_ENV_PATTERN = /KEY|SECRET|TOKEN/i
|
||||
|
||||
/**
|
||||
* The ambient parent environment minus credential-shaped names and minus all
|
||||
* `DSH_*` names — the canonical base every harness child starts from. `PATH`,
|
||||
* `HOME`, locale, and proxy variables survive, so child CLIs run normally;
|
||||
* harness identity never leaks implicitly (a child that needs current `DSH_*`
|
||||
* facts receives them through {@link SubprocessSpawnSpec.dshEnv}, and a
|
||||
* deliberately forwarded credential goes through an explicit env layer, which
|
||||
* merges after this scrub). Exported as a plain function so spawners that
|
||||
* cannot route through the service (node-pty backends, SDK-managed
|
||||
* transports) share the one scrub definition.
|
||||
* @returns a fresh environment object safe to hand to a child spawn.
|
||||
*/
|
||||
export function scrubbedParentEnv(): Record<string, string> {
|
||||
const env: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.startsWith(DSH_ENV_PREFIX)) env[key] = value
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
subprocess: SubprocessService
|
||||
@@ -37,13 +75,17 @@ declare module 'cordis' {
|
||||
*
|
||||
* Implementations must honor these semantics:
|
||||
* - {@link spawn} returns immediately with a live handle; `done` resolves at
|
||||
* process close and rejects only for spawn-level failures.
|
||||
* - Output readers are offset-based and non-consuming, so independent readers
|
||||
* never consume one another's output; lossy reads report truncation and the
|
||||
* spill file holding the complete stream when one exists.
|
||||
* - {@link SubprocessHandle.kill} and the spec's abort signal escalate
|
||||
* SIGTERM→grace→SIGKILL across the whole process group.
|
||||
* - Disposal kills all still-running managed processes and awaits their exit.
|
||||
* process close with exit facts and rejects only for spawn-level failures.
|
||||
* - Collect-mode readers are offset-based and non-consuming, so independent
|
||||
* readers never consume one another's output; lossy reads report truncation
|
||||
* and the spill file holding the complete stream when one exists. Piped
|
||||
* streams are handed to the caller raw and never buffered here.
|
||||
* - {@link SubprocessHandle.kill} signals without escalation,
|
||||
* {@link SubprocessHandle.terminate} (and the spec's abort signal) escalates
|
||||
* SIGTERM→grace→SIGKILL, and {@link SubprocessHandle.dispose} runs the
|
||||
* cooperative EOF-first ladder — all tree-scoped on every platform.
|
||||
* - Disposal of the service terminates all still-running managed processes
|
||||
* and awaits their exit.
|
||||
*/
|
||||
export abstract class SubprocessService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
@@ -53,8 +95,8 @@ export abstract class SubprocessService extends Service {
|
||||
/**
|
||||
* Start one managed child process from a fully-specified spec; this seam
|
||||
* applies no defaults.
|
||||
* @param spec - argv, directory, limits, grace, cancellation, and environment.
|
||||
* @returns the live process handle (readers, kill, outcome promise).
|
||||
* @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
|
||||
* @returns the live process handle (streams/readers, signalling, outcome promise).
|
||||
*/
|
||||
abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user