index.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /**
  2. * The bash executor seam (`ctx.bash`): an abstract service defining what a bash backend does —
  3. * run commands, manage background tasks — without saying how.
  4. * @module @deepseek-ai/dsh-bash
  5. */
  6. import { Context, Service } from 'cordis'
  7. import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
  8. import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskId, BashTaskListener, BashTaskRead, OwnerToken } from './types.ts'
  9. export { BashTaskId, OwnerToken } from './types.ts'
  10. export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts'
  11. export type {
  12. BashExecRequest,
  13. BashExecSpec,
  14. BashRunResult,
  15. BashSandboxInfo,
  16. BashTask,
  17. BashTaskListener,
  18. BashTaskRead,
  19. BashTaskStatus,
  20. CollectedOutput,
  21. } from './types.ts'
  22. declare module 'cordis' {
  23. interface Context {
  24. bash: BashExecutor
  25. }
  26. }
  27. /**
  28. * Registers one `ctx.bash` implementation. Runtime command failures resolve as
  29. * {@link BashRunResult}; only infrastructure failures reject. Background starts
  30. * return immediately without a timeout, report completion exactly once while
  31. * live, and remain cancellable by signal or {@link kill}. Output reads are
  32. * incremental and flag lost buffered data; disposal kills and awaits all tasks.
  33. */
  34. export abstract class BashExecutor extends Service {
  35. private listeners = new Set<BashTaskListener>()
  36. private listenersClosed = false
  37. constructor(ctx: Context) {
  38. super(ctx, 'bash')
  39. ctx.effect(() => () => {
  40. // Close the listener registry before subclass teardown so late task
  41. // completions (e.g. from kills issued during dispose) stay silent.
  42. this.listenersClosed = true
  43. this.listeners.clear()
  44. }, 'bash listener teardown')
  45. }
  46. /**
  47. * The sandbox mode this executor confines commands under BY DEFAULT, or `undefined` when it
  48. * does not sandbox at all — the capability fact the tool and ACP layers read to advertise
  49. * sandbox controls honestly.
  50. * A session or call may override this default, so widening is evaluated per
  51. * execution rather than encoded in this getter.
  52. * @returns the configured default mode of a sandboxing executor;
  53. * `undefined` for an executor that never confines.
  54. */
  55. get sandboxMode(): SandboxMode | undefined {
  56. return undefined
  57. }
  58. /**
  59. * Apply implementation-owned defaults and caps to a request before execution.
  60. * @param request - the caller's request; omitted fields get this
  61. * implementation's defaults, capped fields are clamped.
  62. * @returns the fully-specified spec to hand to {@link run}/{@link start}.
  63. */
  64. abstract resolve(request: BashExecRequest): BashExecSpec
  65. /**
  66. * Run a command in the foreground; resolves when it finishes.
  67. * @param spec - a resolved spec from {@link resolve}, never a raw request.
  68. * @returns the outcome; nonzero exits, timeout kills, and abort kills
  69. * resolve with a descriptive result rather than reject.
  70. */
  71. abstract run(spec: BashExecSpec): Promise<BashRunResult>
  72. /**
  73. * Start a background task and return its handle immediately.
  74. * @param spec - a resolved spec from {@link resolve}, never a raw request.
  75. * @returns the live task handle; completion fires {@link onTaskDone}.
  76. */
  77. abstract start(spec: BashExecSpec): BashTask
  78. /**
  79. * Look up a background task by id.
  80. * @param id - the task id to look up.
  81. * @returns the tracked task, or undefined for an id this executor never issued.
  82. */
  83. abstract get(id: BashTaskId): BashTask | undefined
  84. /**
  85. * The opaque OWNER token recorded for a background task at {@link start} (from the {@link
  86. * BashExecSpec}'s `owner`), or `undefined` for an unknown id OR a known-but-ownerless task.
  87. * The executor stores the token without interpreting policy; keeping it here
  88. * lets ownership survive a consumer-plugin reload.
  89. * @param id - the background task id to look up ownership for.
  90. * @returns the token recorded at start, verbatim; undefined for an unknown
  91. * id or a known-but-ownerless task.
  92. */
  93. abstract ownerOf(id: BashTaskId): OwnerToken | undefined
  94. /**
  95. * All tracked background tasks (insertion order).
  96. * @returns every task this executor started, running or finished.
  97. */
  98. abstract list(): BashTask[]
  99. /**
  100. * Read output produced since the previous read. Throws for unknown ids.
  101. * @param id - the task to read from.
  102. * @returns the incremental read; consecutive reads never re-deliver output.
  103. */
  104. abstract readOutput(id: BashTaskId): BashTaskRead
  105. /**
  106. * Kill a running background task. Returns false when it had already
  107. * finished (no-op). Throws for unknown ids.
  108. * @param id - the task to kill.
  109. * @returns true when this call killed it, false when it had already finished.
  110. */
  111. abstract kill(id: BashTaskId): boolean
  112. /**
  113. * Register a background-task completion listener (disposed with the
  114. * calling fiber). Listeners never fire after this service is disposed.
  115. * @param listener - called exactly once per task completion.
  116. * @returns the disposer that unregisters the listener.
  117. */
  118. onTaskDone(listener: BashTaskListener): () => void {
  119. const dispose = this.ctx.effect(() => {
  120. this.listeners.add(listener)
  121. return () => this.listeners.delete(listener)
  122. }, 'bash.onTaskDone()')
  123. return () => void dispose()
  124. }
  125. /** For implementations: notify listeners that `task` completed. Listener
  126. * exceptions are contained (logged) — one bad listener must not reject
  127. * `BashTask.done` or starve the listeners after it. */
  128. protected notifyTaskDone(task: BashTask): void {
  129. if (this.listenersClosed) return
  130. for (const listener of this.listeners) {
  131. try {
  132. listener(task)
  133. } catch (error: unknown) {
  134. // Listener bugs are reported, never propagated into task.done.
  135. console.error('bash onTaskDone listener threw:', error)
  136. }
  137. }
  138. }
  139. }
  140. export default BashExecutor