index.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  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|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). Exported as a plain function so spawners
  45. * that cannot route through the service (node-pty backends, SDK-managed
  46. * transports) share the one scrub definition.
  47. * @returns a fresh environment object safe to hand to a child spawn.
  48. */
  49. export function scrubbedParentEnv(): Record<string, string> {
  50. const env: Record<string, string> = {}
  51. for (const [key, value] of Object.entries(process.env)) {
  52. if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.startsWith(DSH_ENV_PREFIX)) env[key] = value
  53. }
  54. return env
  55. }
  56. declare module 'cordis' {
  57. interface Context {
  58. subprocess: SubprocessService
  59. }
  60. }
  61. /**
  62. * Abstract subprocess service. Subclass, implement {@link spawn}, and load the
  63. * subclass as a plugin — it registers as `ctx.subprocess` (one implementation
  64. * per context; loading a second throws, which is cordis' standard
  65. * duplicate-service behavior).
  66. *
  67. * Implementations must honor these semantics:
  68. * - {@link spawn} returns immediately with a live handle; `done` resolves at
  69. * process close with exit facts and rejects only for spawn-level failures.
  70. * - Collect-mode readers are offset-based and non-consuming, so independent
  71. * readers never consume one another's output; lossy reads report truncation
  72. * and the spill file holding the complete stream when one exists. Piped
  73. * streams are handed to the caller raw and never buffered here.
  74. * - {@link SubprocessHandle.terminate} (and the spec's abort signal) escalates
  75. * SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every
  76. * platform. {@link SubprocessHandle.waitForExit} observes whole-tree
  77. * liveness, so a consumer-owned teardown ladder can hold each tier on real
  78. * quiescence.
  79. * - Disposal of the service terminates all still-running managed processes
  80. * and awaits their exit.
  81. */
  82. export abstract class SubprocessService extends Service {
  83. constructor(ctx: Context) {
  84. super(ctx, 'subprocess')
  85. }
  86. /**
  87. * Start one managed child process from a fully-specified spec; this seam
  88. * applies no defaults.
  89. * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
  90. * @returns the live process handle (streams/readers, signalling, outcome promise).
  91. */
  92. abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
  93. }
  94. export default SubprocessService