index.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. /**
  2. * Service Definition for the subprocess capability seam (`ctx.subprocess`): execution-world executable lookup,
  3. * fully specified provider-managed process ranges 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 { proxyEnvironmentForChild } from '@deepseek-ai/dsh-http-proxy'
  12. import { DSH_ENV_PREFIX } from './types.ts'
  13. import type { SubprocessHandle, SubprocessSpawnSpec } from './types.ts'
  14. import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from './types.ts'
  15. export { DSH_ENV_PREFIX } from './types.ts'
  16. export type {
  17. CollectedOutput,
  18. DshEnvironment,
  19. DshEnvironmentKey,
  20. SubprocessCollect,
  21. SubprocessCollectedOutputs,
  22. SubprocessHandle,
  23. SubprocessOutcome,
  24. SubprocessOutputMode,
  25. SubprocessOutputRead,
  26. SubprocessOutputReader,
  27. SubprocessSpawnSpec,
  28. SubprocessStdinMode,
  29. SubprocessStdio,
  30. SubprocessTerminalForeground,
  31. SubprocessTerminalHandle,
  32. SubprocessTerminalSignal,
  33. SubprocessTerminalSpawnSpec,
  34. } from './types.ts'
  35. /**
  36. * Credential-shaped environment names are NOT forwarded to children (the
  37. * harness's own `DEEPSEEK_API_KEY`/secrets must not leak into a spawned
  38. * process implicitly). One heuristic for every in-repo spawner; a
  39. * deliberately supplied entry survives because explicit env layers merge
  40. * after the scrub.
  41. */
  42. export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i
  43. /**
  44. * The ambient parent environment minus credential-shaped names and minus all
  45. * `DSH_*` names — the canonical base every harness child starts from. `PATH`,
  46. * `HOME`, locale, and proxy variables survive, so child CLIs run normally;
  47. * harness identity never leaks implicitly (a deliberately forwarded
  48. * credential or current `DSH_*` fact goes through the spec's explicit `env`,
  49. * which merges after this scrub). Both scrubs match case-insensitively:
  50. * Windows environment names are case-insensitive, so a parent `dsh_*` entry
  51. * would otherwise survive and read back as `$env:DSH_*` in the child;
  52. * deliberate lowercase `dsh_*` names on POSIX are implausible. Exported as a plain function so spawners
  53. * that cannot route through the service (node-pty backends, SDK-managed
  54. * transports) share the one scrub definition.
  55. *
  56. * When a proxy is active the result also carries the resolved proxy names and the flag a child Node
  57. * needs to honor them, so a child inherits the same routing as its parent.
  58. * @returns a fresh environment object safe to hand to a child spawn.
  59. */
  60. export function scrubbedParentEnv(): Record<string, string> {
  61. const env: Record<string, string> = {}
  62. for (const [key, value] of Object.entries(process.env)) {
  63. if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key) && !key.toUpperCase().startsWith(DSH_ENV_PREFIX)) env[key] = value
  64. }
  65. // A child Node ignores the inherited proxy variables unless the flag this adds is set, so an MCP
  66. // stdio server or subagent CLI would connect directly while its parent proxies. The same overlay
  67. // restores each proxy name to what the user exported, undoing this process's own normalization —
  68. // `undefined` removes a name the user never set.
  69. for (const [name, value] of Object.entries(proxyEnvironmentForChild())) {
  70. if (value === undefined) Reflect.deleteProperty(env, name)
  71. else env[name] = value
  72. }
  73. return env
  74. }
  75. declare module '@deepseek-ai/cordis' {
  76. interface Context {
  77. subprocess: SubprocessRuntime
  78. }
  79. }
  80. /**
  81. * Abstract subprocess service. Subclass, implement {@link spawn}, and load the
  82. * subclass as a plugin — it registers as `ctx.subprocess` (one implementation
  83. * per context; loading a second throws, which is cordis' standard
  84. * duplicate-service behavior).
  85. *
  86. * Implementations must honor these semantics:
  87. * - Executable paths belong to one execution world shared with the mounted
  88. * filesystem provider.
  89. * - {@link spawn} returns a live handle synchronously. Target identity remains
  90. * provider-private; `done` resolves with the spawned command's exit facts and
  91. * may reject for spawn or provider failures.
  92. * - Collect-mode readers are offset-based and non-consuming, so independent
  93. * readers never consume one another's output; lossy reads report truncation
  94. * and the spill file holding the complete stream when one exists. Piped
  95. * streams are handed to the caller raw and never buffered here.
  96. * - {@link SubprocessHandle.terminate} (and the spec's abort signal) starts the
  97. * provider's documented procedure against its managed range.
  98. * {@link SubprocessHandle.waitForExit} observes that same range so a
  99. * consumer-owned teardown ladder can hold each tier on real quiescence; each
  100. * provider documents its signalling and observability limits.
  101. * - Disposal of the service terminates all still-running managed processes
  102. * and awaits their exit.
  103. * - {@link spawnTerminal} owns terminal allocation, text transport,
  104. * foreground groups, signalling, and whole-session quiescence behind one
  105. * awaited termination method; readiness and persistent-shell policy stay
  106. * in the PTY consumer. Its output stream ends after queued terminal output
  107. * when the top-level process exits.
  108. */
  109. export abstract class SubprocessRuntime extends Service {
  110. constructor(ctx: Context) {
  111. super(ctx, 'subprocess')
  112. }
  113. /**
  114. * Resolve one configured executable in this provider's execution world.
  115. * Absolute paths are verified; bare names use the provider's scrubbed PATH
  116. * plus explicit environment overrides. Relative paths containing separators
  117. * are rejected: the resolution base is undefined, so providers fail loud
  118. * instead of guessing.
  119. * @param command - absolute executable path or bare PATH name.
  120. * @param env - explicit environment entries used for lookup.
  121. * @param signal - aborts remote or local lookup.
  122. * @returns a canonical executable path.
  123. */
  124. abstract resolveExecutable(
  125. command: string,
  126. env?: Readonly<Record<string, string>>,
  127. signal?: AbortSignal,
  128. ): Promise<string>
  129. /**
  130. * Start one managed child process from a fully-specified spec; this seam
  131. * applies no defaults.
  132. * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
  133. * @returns the live process handle (streams/readers, signalling, outcome promise).
  134. * @throws synchronously when pre-aborted or when argv, cwd, environment, or grace is invalid before handle creation.
  135. */
  136. abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
  137. /**
  138. * Allocate a real terminal and start one owned process session. This is the
  139. * only non-pipe process primitive: implementations own terminal byte I/O,
  140. * foreground groups, signals, and whole-session quiescence.
  141. * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
  142. * @returns the live terminal handle after allocation succeeds.
  143. */
  144. abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
  145. }
  146. export default SubprocessRuntime