Parcourir la source

fix(process): close review gaps from Codex round 1

- Mount LocalProcessManager in the sandbox e2e compositions (bwrap/landlock/
  seatbelt + the spine multi-project e2e) and add the spine demo's
  dsh-process-local devDependency, so SandboxBashExecutor's new inject
  resolves when those suites are enabled.
- Extend the Windows test/coverage skip to packages/process/* — the POSIX
  process-group suite moved there from packages/bash.
- Update the stale disposal contract: the bash seam JSDoc, BashProcess JSDoc,
  and core bash doc (en+zh) now state that composition teardown (the process
  manager's disposal) owns kill-and-await, and an executor-only reload leaves
  background processes running.
- Record ctx.processes in the architecture capability table and extension map
  (en+zh) and the root AGENTS.md layout tree; reword the timeout-library note
  so it describes where the plumbing and classification live today.
Tianyi Cui il y a 1 mois
Parent
commit
fb0d4ba564

+ 1 - 0
AGENTS.md

@@ -15,6 +15,7 @@ packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   prompt/      workspace instructions
   llm/         LLM seam + the DeepSeek adapters (hand-rolled + pi-ai design twin)
   bash/        bash executor seam + local impl + model-facing bash tools
+  process/     child-process manager seam + local process-group impl
   pty/         persistent PTY seam/backend/tools
   fs/          filesystem seam + local impl + policy gate + read/write/edit tools
   lsp/         language-server seam + local stdio provider + model-facing lsp tool

+ 2 - 2
docs/architecture.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-architecture.md: b426891c0483f42a64b597632cf1871aff79ca2d
-architecture.zh.md: 13feefb6854e79ddee38602902d325a789fd7744
+architecture.md: ca2d1c70342fcef35deca7e247863511fb7c35b8
+architecture.zh.md: c3107b23f96497a5ab2184d660d085f3e8a5e527

+ 2 - 1
docs/architecture.md

@@ -28,6 +28,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts with package-contributed servi
 | `ctx.llm` | [`llm/`](../packages/llm/README.md) | adapter registry and streaming model calls |
 | `ctx.tokenMeter` | [`llm/token-meter`](../packages/llm/token-meter/README.md) | singleton replay-aware request/surface pressure |
 | `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution |
+| `ctx.processes` | [`process/`](../packages/process/README.md) | managed child-process groups under the bash executors |
 | `ctx.pty` | [`pty/`](../packages/pty/README.md) | owner-scoped persistent terminal sessions |
 | `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | same-world process confinement (argv wrapping, per-call policy) |
 | `ctx.sandboxPolicy` | [`sandbox/`](../packages/sandbox/README.md) | shared sandbox policy home |
@@ -177,7 +178,7 @@ New behavior attaches to a documented extension point; a loop change updates thi
 |---|---|
 | Add a model provider | register an adapter on `ctx.llm` |
 | Add a model-facing capability | register on `ctx.tools`; schemas enter prompt assembly |
-| Add shell execution | implement and register a `ctx.bash` backend |
+| Add shell execution | implement and register a `ctx.bash` backend (the local one spawns through `ctx.processes`) |
 | Add persistent terminal execution | register a `ctx.pty` backend and `dsh-tool-pty` |
 | Add a human command | register on `ctx.commands`; adapters discover and dispatch it without a model turn |
 | Add background work | register on `ctx.tasks`; generic `task_*` tools collect or stop it |

+ 2 - 1
docs/architecture.zh.md

@@ -28,6 +28,7 @@
 | `ctx.llm` | [`llm/`](../packages/llm/README.md) | 适配器注册表和模型流式调用 |
 | `ctx.tokenMeter` | [`llm/token-meter`](../packages/llm/token-meter/README.md) | 感知回放的单实例请求压力和会话表面压力 |
 | `ctx.bash` | [`bash/`](../packages/bash/README.md) | 前台和后台命令执行 |
+| `ctx.processes` | [`process/`](../packages/process/README.md) | bash 执行器之下受管理的子进程组 |
 | `ctx.pty` | [`pty/`](../packages/pty/README.md) | 按 owner 隔离的持久化终端会话 |
 | `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | 同一执行环境内的进程限制(argv 包装、逐调用策略) |
 | `ctx.sandboxPolicy` | [`sandbox/`](../packages/sandbox/README.md) | 共享沙箱策略归属点 |
@@ -177,7 +178,7 @@ forever:
 |---|---|
 | 添加模型提供方 | 在 `ctx.llm` 上注册适配器 |
 | 添加面向模型的功能 | 在 `ctx.tools` 上注册;schema 进入提示词组装流程 |
-| 添加 shell 执行 | 实现并注册 `ctx.bash` 后端 |
+| 添加 shell 执行 | 实现并注册 `ctx.bash` 后端(本地后端通过 `ctx.processes` 生成进程) |
 | 添加持久化终端执行 | 注册 `ctx.pty` 后端和 `dsh-tool-pty` |
 | 添加用户命令 | 在 `ctx.commands` 上注册;适配器无需模型轮次即可发现并分派该命令 |
 | 添加后台工作 | 在 `ctx.tasks` 上注册;通用 `task_*` 工具负责收集或停止 |

+ 2 - 2
docs/cordis-catalog/services.md

@@ -257,7 +257,7 @@ Implementations must honor these semantics:
 - run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
 - start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
 - BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
-- Disposal kills all running background processes and awaits their exit.
+- A still-running background process is stopped and awaited when its owning composition tears down. With the process-manager seam that boundary is `ctx.processes` disposal, so a background process survives an executor-only reload.
 
 ```ts cordis-catalog
 /**
@@ -286,7 +286,7 @@ abstract start(spec: BashExecSpec): BashProcess
 
 Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashProcess](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md)
 
-Source: [`packages/bash/bash/src/index.ts:48`](../../packages/bash/bash/src/index.ts)
+Source: [`packages/bash/bash/src/index.ts:51`](../../packages/bash/bash/src/index.ts)
 
 ## `ctx.bashEnv` — `BashEnvRegistry`
 

+ 2 - 2
docs/core-data-structures/bash.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-bash.md: e092bfb1bf1aa23f7eeeda41a16a1625d1698201
-bash.zh.md: 7ae60b9f38e6ca5c72e349e141132ae6ec72913d
+bash.md: 0c92addf4778fa780cd432444549bfc36eeb763c
+bash.zh.md: cd9d94055627937d03107ead5a4ffaeaa945d187

+ 4 - 3
docs/core-data-structures/bash.md

@@ -170,8 +170,9 @@ One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (o
 ```ts type-equiv
 /**
  * A background process handle returned by {@link BashExecutor.start}. It is the
- * only access path; buffered output remains readable after exit. Executor
- * disposal kills running processes and awaits {@link done}.
+ * only access path; buffered output remains readable after exit. Composition
+ * teardown (the process manager's disposal) kills running processes and
+ * awaits {@link done}; an executor-only reload leaves them running.
  */
 interface BashProcess {
   /** Process lifecycle state (settled exactly once). */
@@ -216,4 +217,4 @@ interface BashProcessRead {
 
 ## The service
 
-`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns process groups, timeout/abort handling, bounded collectors, spill files, credential scrubbing, and disposal quiescence. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md).
+`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [process manager](process.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md).

+ 4 - 3
docs/core-data-structures/bash.zh.md

@@ -170,8 +170,9 @@ interface BashSandboxInfo {
 ```ts type-equiv
 /**
  * A background process handle returned by {@link BashExecutor.start}. It is the
- * only access path; buffered output remains readable after exit. Executor
- * disposal kills running processes and awaits {@link done}.
+ * only access path; buffered output remains readable after exit. Composition
+ * teardown (the process manager's disposal) kills running processes and
+ * awaits {@link done}; an executor-only reload leaves them running.
  */
 interface BashProcess {
   /** Process lifecycle state (settled exactly once). */
@@ -216,4 +217,4 @@ interface BashProcessRead {
 
 ## 服务
 
-`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有进程组、超时/中止处理、有界收集器、spill 文件、凭据清除以及 dispose(资源释放)后完全停稳。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。
+`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并;进程组、有界收集器、spill 文件、凭据清除与 dispose(资源释放)后完全停稳归[进程管理器](process.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。

+ 2 - 0
packages/bash/bash-sandbox/tests/bwrap.e2e.ts

@@ -9,6 +9,7 @@ import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
 import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
 import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
 import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
+import LocalProcessManager from '@deepseek-ai/dsh-process-local'
 
 /**
  * Keyless integration of the real provider and executor through public run/start paths. With
@@ -42,6 +43,7 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w
   ctx = new Context()
   await ctx.plugin(LocalSandboxProvider, {})
   await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
+  await ctx.plugin(LocalProcessManager)
   await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
   return ctx.bash as SandboxBashExecutor
 }

+ 2 - 0
packages/bash/bash-sandbox/tests/landlock.e2e.ts

@@ -9,6 +9,7 @@ import { launcherPath } from 'node-addon-landlock-run'
 import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
 import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
 import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
+import LocalProcessManager from '@deepseek-ai/dsh-process-local'
 
 /**
  * KEYLESS consumer-integration proof: the REAL `LocalSandboxProvider` (bwrap
@@ -47,6 +48,7 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w
   await ctx.plugin(LocalSandboxProvider, {})
   ;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false }
   await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
+  await ctx.plugin(LocalProcessManager)
   await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
   return ctx.bash as SandboxBashExecutor
 }

+ 2 - 0
packages/bash/bash-sandbox/tests/seatbelt.e2e.ts

@@ -9,6 +9,7 @@ import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
 import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
 import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
 import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
+import LocalProcessManager from '@deepseek-ai/dsh-process-local'
 
 /**
  * Keyless macOS integration of the real provider and executor through public run/start paths.
@@ -41,6 +42,7 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w
   await ctx.plugin(LocalSandboxProvider, {})
   ;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
   await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace })
+  await ctx.plugin(LocalProcessManager)
   await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 })
   return ctx.bash as SandboxBashExecutor
 }

+ 4 - 1
packages/bash/bash/src/index.ts

@@ -43,7 +43,10 @@ declare module 'cordis' {
  *   failures settle as `killed` with the error on stderr.
  * - {@link BashProcess.readOutput} is incremental: consecutive reads never
  *   repeat output. Lossy reads report truncation and available spill files.
- * - Disposal kills all running background processes and awaits their exit.
+ * - A still-running background process is stopped and awaited when its
+ *   owning composition tears down. With the process-manager seam that
+ *   boundary is `ctx.processes` disposal, so a background process survives
+ *   an executor-only reload.
  */
 export abstract class BashExecutor extends Service {
   constructor(ctx: Context) {

+ 3 - 2
packages/bash/bash/src/types.ts

@@ -153,8 +153,9 @@ export interface BashProcessRead {
 
 /**
  * A background process handle returned by {@link BashExecutor.start}. It is the
- * only access path; buffered output remains readable after exit. Executor
- * disposal kills running processes and awaits {@link done}.
+ * only access path; buffered output remains readable after exit. Composition
+ * teardown (the process manager's disposal) kills running processes and
+ * awaits {@link done}; an executor-only reload leaves them running.
  */
 export interface BashProcess {
   /** Process lifecycle state (settled exactly once). */

+ 1 - 0
packages/examples/agent-spine-demo/package.json

@@ -65,6 +65,7 @@
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-paths": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
+    "@deepseek-ai/dsh-process-local": "workspace:^",
     "@deepseek-ai/dsh-sandbox-local": "workspace:^",
     "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",

+ 2 - 0
packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts

@@ -5,6 +5,7 @@ import { basename, join } from 'node:path'
 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
 import { Context } from 'cordis'
 import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
+import LocalProcessManager from '@deepseek-ai/dsh-process-local'
 import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
 import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox'
 import { CallId } from '@deepseek-ai/dsh-llm'
@@ -52,6 +53,7 @@ beforeEach(async () => {
   ctx = new Context()
   await ctx.plugin(LocalSandboxProvider, {})
   await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: fallbackRoot })
+  await ctx.plugin(LocalProcessManager)
   await ctx.plugin(SandboxBashExecutor, { cwd: fallbackRoot, timeoutMs: 30_000 })
   await ctx.plugin(SandboxedFileSystem, { cwd: fallbackRoot })
   await ctx.plugin(agentSpine, {

+ 3 - 0
pnpm-lock.yaml

@@ -1658,6 +1658,9 @@ importers:
       '@deepseek-ai/dsh-paths':
         specifier: workspace:^
         version: link:../../util/paths
+      '@deepseek-ai/dsh-process-local':
+        specifier: workspace:^
+        version: link:../../process/process-local
       '@deepseek-ai/dsh-sandbox-local':
         specifier: workspace:^
         version: link:../../sandbox/sandbox-local

+ 1 - 0
vitest.config.ts

@@ -11,6 +11,7 @@ const windowsUnsupportedPackages = process.platform === 'win32'
   ? [
       'packages/bash/*',
       'packages/hooks/*',
+      'packages/process/*',
       'packages/pty/pty-local',
       'packages/sandbox/sandbox-local',
       'packages/sdk/create-sdk',