Files
deepseek-harness/packages/subprocess/subprocess/src/index.ts
Huanqi Cao b2838fb2be fix(pwsh): close the remaining review threads
- scrubbedParentEnv folds case before the DSH_ prefix check (Windows env
  names are case-insensitive; a parent dsh_* entry read back as \*
  in the child) and the service spec pins the lowercase probe.
- The acp.snapshot.ts pwsh probe follows resolvePwshPath() like the package
  suites, so a Windows host with only an install-location pwsh still runs
  the scenario.
- pwsh-tool-turn is re-recorded around [Console]::Out.Write('PWSH_OK'):
  the fixture carries no platform newline, so one recording replays on
  Windows and POSIX alike (record + refresh; replay-verified keyless).
- The pwsh-local Known Limitations bullet drops the self-defeating no-op
  advice: & { } is scoped to param(...), using/#requires scripts run from a
  file (both languages, pairing re-recorded).
- The capability-seams graph moves ctx.bashEnv ownership to bash-env and
  lists pwsh-local/tool-pwsh on the ctx.bash seam (source updated,
  docs regenerated).
- The tool-bash presenter fixture retires the stale 'command aborted'
  literal for the shipped 'tool call aborted' message.
2026-08-02 21:46:53 +08:00

106 lines
4.4 KiB
TypeScript

/**
* 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. Command defaulting, shell
* semantics, deadlines, teardown ladders, 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'
export type {
CollectedOutput,
DshEnvironment,
DshEnvironmentKey,
SubprocessCollect,
SubprocessCollectedOutputs,
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|PASSWORD|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 deliberately forwarded
* credential or current `DSH_*` fact goes through the spec's explicit `env`,
* which merges after this scrub). Both scrubs match case-insensitively:
* Windows environment names are case-insensitive, so a parent `dsh_*` entry
* would otherwise survive and read back as `$env:DSH_*` in the child;
* deliberate lowercase `dsh_*` names on POSIX are implausible. 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.toUpperCase().startsWith(DSH_ENV_PREFIX)) env[key] = value
}
return env
}
declare module 'cordis' {
interface Context {
subprocess: SubprocessService
}
}
/**
* Abstract subprocess service. Subclass, implement {@link spawn}, and load the
* subclass as a plugin — it registers as `ctx.subprocess` (one implementation
* per context; loading a second throws, which is cordis' standard
* duplicate-service behavior).
*
* Implementations must honor these semantics:
* - {@link spawn} returns immediately with a live handle; `done` resolves at
* 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.terminate} (and the spec's abort signal) escalates
* SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every
* platform. {@link SubprocessHandle.waitForExit} observes whole-tree
* liveness, so a consumer-owned teardown ladder can hold each tier on real
* quiescence.
* - Disposal of the service terminates all still-running managed processes
* and awaits their exit.
*/
export abstract class SubprocessService extends Service {
constructor(ctx: Context) {
super(ctx, 'subprocess')
}
/**
* Start one managed child process from a fully-specified spec; this seam
* applies no defaults.
* @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
}
export default SubprocessService