index.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. /**
  2. * The subprocess seam (`ctx.subprocess`): spawn fully-specified commands into
  3. * managed process trees with Node-shaped stdio dispositions — raw pipes for
  4. * protocol streams, inherit for diagnostics, bounded spill-backed collection
  5. * for batch output — plus tree-scoped signalling. Command defaulting, shell
  6. * semantics, deadlines, teardown ladders, framing, and presentation belong to
  7. * consumers; the bash executor seam is the owning template. The local implementation lives in
  8. * `@deepseek-ai/dsh-subprocess-local`.
  9. * @module @deepseek-ai/dsh-subprocess
  10. */
  11. import { Context, Service } from 'cordis'
  12. import { DSH_ENV_PREFIX } from './types.ts'
  13. import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
  14. export { DSH_ENV_PREFIX } from './types.ts'
  15. export type {
  16. CollectedOutput,
  17. DshEnvironment,
  18. DshEnvironmentKey,
  19. SubprocessCollect,
  20. SubprocessCollectedOutputs,
  21. SubprocessHandle,
  22. SubprocessOutcome,
  23. SubprocessOutputMode,
  24. SubprocessOutputRead,
  25. SubprocessOutputReader,
  26. SubprocessSpawnSpec,
  27. SubprocessStdinMode,
  28. SubprocessStdio,
  29. } from './types.ts'
  30. /**
  31. * Credential-shaped environment names are NOT forwarded to children (the
  32. * harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a spawned
  33. * process implicitly). One heuristic for every in-repo spawner; a
  34. * deliberately supplied entry survives because explicit env layers merge
  35. * after the scrub.
  36. */
  37. export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i
  38. /**
  39. * The ambient parent environment minus credential-shaped names and minus all
  40. * `DSH_*` names — the canonical base every harness child starts from. `PATH`,
  41. * `HOME`, locale, and proxy variables survive, so child CLIs run normally;
  42. * harness identity never leaks implicitly (a deliberately forwarded
  43. * credential or current `DSH_*` fact goes through the spec's explicit `env`,
  44. * which merges after this scrub). Both scrubs match case-insensitively:
  45. * Windows environment names are case-insensitive, so a parent `dsh_*` entry
  46. * would otherwise survive and read back as `$env:DSH_*` in the child;
  47. * deliberate lowercase `dsh_*` names on POSIX are implausible. Exported as a plain function so spawners
  48. * that cannot route through the service (node-pty backends, SDK-managed
  49. * transports) share the one scrub definition.
  50. * @returns a fresh environment object safe to hand to a child spawn.
  51. */
  52. export function scrubbedParentEnv(): Record<string, string> {
  53. const env: Record<string, string> = {}
  54. for (const [key, value] of Object.entries(process.env)) {
  55. if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX)) env[key] = value
  56. }
  57. return env
  58. }
  59. declare module 'cordis' {
  60. interface Context {
  61. subprocess: SubprocessService
  62. }
  63. }
  64. /**
  65. * Abstract subprocess service. Subclass, implement {@link spawn}, and load the
  66. * subclass as a plugin — it registers as `ctx.subprocess` (one implementation
  67. * per context; loading a second throws, which is cordis' standard
  68. * duplicate-service behavior).
  69. *
  70. * Implementations must honor these semantics:
  71. * - {@link spawn} returns immediately with a live handle; `done` resolves at
  72. * process close with exit facts and rejects only for spawn-level failures.
  73. * - Collect-mode readers are offset-based and non-consuming, so independent
  74. * readers never consume one another's output; lossy reads report truncation
  75. * and the spill file holding the complete stream when one exists. Piped
  76. * streams are handed to the caller raw and never buffered here.
  77. * - {@link SubprocessHandle.terminate} (and the spec's abort signal) escalates
  78. * SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every
  79. * platform. {@link SubprocessHandle.waitForExit} observes whole-tree
  80. * liveness, so a consumer-owned teardown ladder can hold each tier on real
  81. * quiescence.
  82. * - Disposal of the service terminates all still-running managed processes
  83. * and awaits their exit.
  84. */
  85. export abstract class SubprocessService extends Service {
  86. constructor(ctx: Context) {
  87. super(ctx, 'subprocess')
  88. }
  89. /**
  90. * Start one managed child process from a fully-specified spec; this seam
  91. * applies no defaults.
  92. * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
  93. * @returns the live process handle (streams/readers, signalling, outcome promise).
  94. */
  95. abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
  96. }
  97. export default SubprocessService