Delete design-session citations (decision/audit/plan ordinals, stack positions), change narration, review choreography, and reviewer-addressed justification from comments, JSDoc, docs, READMEs, Agent Notes, tests, and generator templates; restate every affected fact as current-state contract prose. Fix generated docs at their sources and regenerate the catalogs and cordis-surface regions; re-paste type-equiv blocks; update every bilingual counterpart and re-record the pairs. Record the citation rule in the committed-artifact-citations Agent Note.
143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
/**
|
|
* Service Definition for the subprocess capability seam (`ctx.subprocess`): execution-world executable lookup,
|
|
* fully specified managed process trees with raw or
|
|
* collected stdio, and one terminal-process primitive. Command defaulting,
|
|
* shell semantics, deadlines, protocol framing, terminal readiness, and
|
|
* presentation belong to consumers. 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'
|
|
import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } 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,
|
|
SubprocessTerminalForeground,
|
|
SubprocessTerminalHandle,
|
|
SubprocessTerminalSignal,
|
|
SubprocessTerminalSpawnSpec,
|
|
} 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:
|
|
* - Executable paths belong to one execution world shared with the mounted
|
|
* filesystem provider.
|
|
* - {@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.
|
|
* - {@link spawnTerminal} owns terminal allocation, text transport,
|
|
* foreground groups, signalling, and whole-session quiescence behind one
|
|
* awaited termination method; readiness and persistent-shell policy stay
|
|
* in the PTY consumer. Its output stream ends after queued terminal output
|
|
* when the top-level process exits.
|
|
*/
|
|
export abstract class SubprocessService extends Service {
|
|
constructor(ctx: Context) {
|
|
super(ctx, 'subprocess')
|
|
}
|
|
|
|
/**
|
|
* Resolve one configured executable in this provider's execution world.
|
|
* Absolute paths are verified; bare names use the provider's scrubbed PATH
|
|
* plus explicit environment overrides. Relative paths containing separators
|
|
* are rejected: the resolution base is undefined, so providers fail loud
|
|
* instead of guessing.
|
|
* @param command - absolute executable path or bare PATH name.
|
|
* @param env - explicit environment entries used for lookup.
|
|
* @param signal - aborts remote or local lookup.
|
|
* @returns a canonical executable path.
|
|
*/
|
|
abstract resolveExecutable(
|
|
command: string,
|
|
env?: Readonly<Record<string, string>>,
|
|
signal?: AbortSignal,
|
|
): Promise<string>
|
|
|
|
/**
|
|
* 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
|
|
|
|
/**
|
|
* Allocate a real terminal and start one owned process session. This is the
|
|
* only non-pipe process primitive: implementations own terminal byte I/O,
|
|
* foreground groups, signals, and complete session-tree cleanup.
|
|
* @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
|
|
* @returns the live terminal handle after allocation succeeds.
|
|
*/
|
|
abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
|
|
}
|
|
|
|
export default SubprocessService
|