index.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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 { 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 a live handle synchronously. Its pid is provider-owned
  78. * and may remain unavailable during asynchronous startup. `done` resolves with
  79. * the spawned command's exit facts and may reject for spawn or provider
  80. * failures.
  81. * - Collect-mode readers are offset-based and non-consuming, so independent
  82. * readers never consume one another's output; lossy reads report truncation
  83. * and the spill file holding the complete stream when one exists. Piped
  84. * streams are handed to the caller raw and never buffered here.
  85. * - {@link SubprocessHandle.terminate} (and the spec's abort signal) starts the
  86. * provider's documented procedure against its managed range.
  87. * {@link SubprocessHandle.waitForExit} observes that same range so consumer
  88. * teardown can await real quiescence; each provider documents its identity,
  89. * signalling, and observability limits.
  90. * - Disposal of the service terminates all still-running managed processes
  91. * and awaits their exit.
  92. * - {@link spawnTerminal} owns terminal allocation, text transport,
  93. * foreground groups, signalling, and whole-session quiescence behind one
  94. * awaited termination method; readiness and persistent-shell policy stay
  95. * in the PTY consumer. Its output stream ends after queued terminal output
  96. * when the top-level process exits.
  97. */
  98. export abstract class SubprocessRuntime extends Service {
  99. constructor(ctx: Context) {
  100. super(ctx, 'subprocess')
  101. }
  102. /**
  103. * Resolve one configured executable in this provider's execution world.
  104. * Absolute paths are verified; bare names use the provider's scrubbed PATH
  105. * plus explicit environment overrides. Relative paths containing separators
  106. * are rejected: the resolution base is undefined, so providers fail loud
  107. * instead of guessing.
  108. * @param command - absolute executable path or bare PATH name.
  109. * @param env - explicit environment entries used for lookup.
  110. * @param signal - aborts remote or local lookup.
  111. * @returns a canonical executable path.
  112. */
  113. abstract resolveExecutable(
  114. command: string,
  115. env?: Readonly<Record<string, string>>,
  116. signal?: AbortSignal,
  117. ): Promise<string>
  118. /**
  119. * Start one managed child process from a fully-specified spec; this seam
  120. * applies no defaults.
  121. * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
  122. * @returns the live process handle (streams/readers, signalling, outcome promise).
  123. */
  124. abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
  125. /**
  126. * Allocate a real terminal and start one owned process session. This is the
  127. * only non-pipe process primitive: implementations own terminal byte I/O,
  128. * foreground groups, signals, and complete session-tree cleanup.
  129. * @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
  130. * @returns the live terminal handle after allocation succeeds.
  131. */
  132. abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
  133. }
  134. export default SubprocessRuntime