index.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /**
  2. * Service Definition for the subprocess capability seam (`ctx.subprocess`): execution-world executable lookup,
  3. * fully specified managed process trees with raw or
  4. * collected stdio, and one terminal-process primitive. Command defaulting,
  5. * shell semantics, deadlines, protocol framing, terminal readiness, and
  6. * presentation belong to consumers. The local implementation lives in
  7. * `@deepseek-ai/dsh-subprocess-local`.
  8. * @module @deepseek-ai/dsh-subprocess
  9. */
  10. import { Context, Service } from '@deepseek-ai/cordis'
  11. import { DSH_ENV_PREFIX } from './types.ts'
  12. import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
  13. import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } 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. SubprocessTerminalForeground,
  30. SubprocessTerminalHandle,
  31. SubprocessTerminalSignal,
  32. SubprocessTerminalSpawnSpec,
  33. } from './types.ts'
  34. /**
  35. * Credential-shaped environment names are NOT forwarded to children (the
  36. * harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a spawned
  37. * process implicitly). One heuristic for every in-repo spawner; a
  38. * deliberately supplied entry survives because explicit env layers merge
  39. * after the scrub.
  40. */
  41. export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i
  42. /**
  43. * The ambient parent environment minus credential-shaped names and minus all
  44. * `DSH_*` names — the canonical base every harness child starts from. `PATH`,
  45. * `HOME`, locale, and proxy variables survive, so child CLIs run normally;
  46. * harness identity never leaks implicitly (a deliberately forwarded
  47. * credential or current `DSH_*` fact goes through the spec's explicit `env`,
  48. * which merges after this scrub). Both scrubs match case-insensitively:
  49. * Windows environment names are case-insensitive, so a parent `dsh_*` entry
  50. * would otherwise survive and read back as `$env:DSH_*` in the child;
  51. * deliberate lowercase `dsh_*` names on POSIX are implausible. Exported as a plain function so spawners
  52. * that cannot route through the service (node-pty backends, SDK-managed
  53. * transports) share the one scrub definition.
  54. * @returns a fresh environment object safe to hand to a child spawn.
  55. */
  56. export function scrubbedParentEnv(): Record<string, string> {
  57. const env: Record<string, string> = {}
  58. for (const [key, value] of Object.entries(process.env)) {
  59. if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX)) env[key] = value
  60. }
  61. return env
  62. }
  63. declare module '@deepseek-ai/cordis' {
  64. interface Context {
  65. subprocess: SubprocessRuntime
  66. }
  67. }
  68. /**
  69. * Abstract subprocess service. Subclass, implement {@link spawn}, and load the
  70. * subclass as a plugin — it registers as `ctx.subprocess` (one implementation
  71. * per context; loading a second throws, which is cordis' standard
  72. * duplicate-service behavior).
  73. *
  74. * Implementations must honor these semantics:
  75. * - Executable paths belong to one execution world shared with the mounted
  76. * filesystem provider.
  77. * - {@link spawn} returns immediately with a live handle; `done` resolves at
  78. * process close with exit facts and rejects only for spawn-level failures.
  79. * - Collect-mode readers are offset-based and non-consuming, so independent
  80. * readers never consume one another's output; lossy reads report truncation
  81. * and the spill file holding the complete stream when one exists. Piped
  82. * streams are handed to the caller raw and never buffered here.
  83. * - {@link SubprocessHandle.terminate} (and the spec's abort signal) escalates
  84. * SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every
  85. * platform. {@link SubprocessHandle.waitForExit} observes whole-tree
  86. * liveness, so a consumer-owned teardown ladder can hold each tier on real
  87. * quiescence.
  88. * - Disposal of the service terminates all still-running managed processes
  89. * and awaits their exit.
  90. * - {@link spawnTerminal} owns terminal allocation, text transport,
  91. * foreground groups, signalling, and whole-session quiescence behind one
  92. * awaited termination method; readiness and persistent-shell policy stay
  93. * in the PTY consumer. Its output stream ends after queued terminal output
  94. * when the top-level process exits.
  95. */
  96. export abstract class SubprocessRuntime extends Service {
  97. constructor(ctx: Context) {
  98. super(ctx, 'subprocess')
  99. }
  100. /**
  101. * Resolve one configured executable in this provider's execution world.
  102. * Absolute paths are verified; bare names use the provider's scrubbed PATH
  103. * plus explicit environment overrides. Relative paths containing separators
  104. * are rejected: the resolution base is undefined, so providers fail loud
  105. * instead of guessing.
  106. * @param command - absolute executable path or bare PATH name.
  107. * @param env - explicit environment entries used for lookup.
  108. * @param signal - aborts remote or local lookup.
  109. * @returns a canonical executable path.
  110. */
  111. abstract resolveExecutable(
  112. command: string,
  113. env?: Readonly<Record<string, string>>,
  114. signal?: AbortSignal,
  115. ): Promise<string>
  116. /**
  117. * Start one managed child process from a fully-specified spec; this seam
  118. * applies no defaults.
  119. * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
  120. * @returns the live process handle (streams/readers, signalling, outcome promise).
  121. */
  122. abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
  123. /**
  124. * Allocate a real terminal and start one owned process session. This is the
  125. * only non-pipe process primitive: implementations own terminal byte I/O,
  126. * foreground groups, signals, and complete session-tree cleanup.
  127. * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
  128. * @returns the live terminal handle after allocation succeeds.
  129. */
  130. abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
  131. }
  132. export default SubprocessRuntime