@deepseek-ai/dsh-tool-bash 0.1.6-alpha.2 → 0.1.7-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/shell/tool-bash/README.md
5
- README.md: 3e01a8b800b5156feecc7e36c45f3490f9abdfc0
6
- README.zh.md: 91eb88c4b08eb515a0fd5f31257d33b6e3033ab9
5
+ README.md: 3068e6d201758a2f66529155de12d1dbf912bae2
6
+ README.zh.md: 032d59efa33b702c8d0b202ebd33e60ae63f6804
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-tool-bash` lets an agent run one-shot `bash` commands and receive stdout, stderr, and exit markers. Each call uses a fresh shell, so cwd, variables, and functions do not persist; `run_in_background` starts long-running work that the agent can inspect with `job_output` and stop with `job_kill`. Commands receive the managed `DSH_*` environment, and sandbox denials can be retried once with wider `sandbox_permissions`, a `justification`, and user approval. Non-zero exits are reported as results, so the agent decides how to respond; use an executor such as `dsh-bash-local` or `dsh-bash-sandbox` and load `dsh-shell-env`.
12
+ `dsh-tool-bash` runs Bash commands and returns stdout, stderr, and exit markers. Each call uses a fresh shell; cwd, variables, and functions do not persist. With a job registry composed, every command is a job from its start: `run_in_background` returns the id at once, a foreground command that outlives its timeout returns the same id, and `job_output`/`job_kill` collect and stop it. Commands receive the managed `DSH_*` environment; sandbox denials can be retried once with wider `sandbox_permissions`, a `justification`, and user approval. Nonzero exits are results for the agent to interpret. Mount an executor such as `dsh-bash-local` or `dsh-bash-sandbox` with `dsh-shell-env`.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -41,11 +41,12 @@ The common path is an executor provider, the environment registry, and this tool
41
41
  - name: '@deepseek-ai/dsh-tool-jobs'
42
42
  ```
43
43
 
44
- The single config field toggles background support.
44
+ The config fields govern the background surface.
45
45
 
46
46
  | Field | Default | Meaning |
47
47
  |---|---|---|
48
- | `enableRunInBackground` | `true` | Expose `run_in_background`; when `false`, forced background calls are rejected |
48
+ | `enableRunInBackground` | `true` | Expose `run_in_background` while a job registry is composed; when `false`, forced background calls are rejected |
49
+ | `promoteOnTimeout` | `true` | Keep a foreground command that reaches its timeout running as its background job instead of killing it |
49
50
 
50
51
  The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-bash) is the exhaustive source for every accepted field and its JSDoc; the generated [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash) carries the full argument schema.
51
52
 
@@ -56,11 +57,15 @@ The tool executes `bash -c <command>` and returns the combined output. Commands
56
57
  <a id="running-long-commands-in-the-background"></a>
57
58
  ### Running long commands in the background
58
59
 
59
- Passing `run_in_background: true` admits a job and returns its id immediately; confinement preparation may still be pending, and no background execution timeout applies. Output is empty until the process is available. Job cancellation aborts preparation and stops any process that arrives afterward; startup failure settles the admitted job as failed. The agent reads its output with `job_output` (non-blocking unless `wait: true`), lists jobs with `job_list`, and stops it with `job_kill`; a finished job notifies the owning agent in-session. Background support needs the generic job runtime (`dsh-jobs-local`) and its control tools (`dsh-tool-jobs`) mounted.
60
+ Passing `run_in_background: true` admits a job and returns its id immediately; confinement preparation may still be pending, and no background execution timeout applies. Output is empty until the process is available. Job cancellation aborts preparation and stops any process that arrives afterward; startup failure settles the admitted job as failed. The agent reads its output with `job_output` (non-blocking unless `wait: true`), lists jobs with `job_list`, and stops it with `job_kill`; a finished job notifies the owning agent in-session. Background support needs the generic job runtime (`dsh-jobs-local`) and its control tools (`dsh-tool-jobs`) mounted. The background job hands the run's non-consuming `observed` readers to the job registry as pull sources; the registry pumps them into the job's output ring at its own cadence (`pumpPollMs` on `dsh-jobs-local`), so the Web client streams live output and the model's `job_output` reads consume the same bytes through a separate cursor. A reader that throws is logged once and its stream stops; the job runs on to its own settlement. The ring is a best-effort live preview: stdout and stderr are copied per poll round, so writes the two streams made inside one poll window appear stdout first rather than in write order.
61
+
62
+ ### Foreground commands as jobs
63
+
64
+ With a job registry composed, a foreground command is registered with `ctx.jobs` at its start and the call waits on that job: the command is listed, streams through `job.list` and `job.follow`, and can be stopped from the Web task list for as long as it runs. A command that finishes within the timeout returns the ordinary foreground result and its job record leaves the registry with it, so the model never sees an id. A command that outlives the timeout keeps running as the job it already was, and the call returns `[still running after <timeoutMs>ms; moved to background job <id>]` plus the job hand-off guidance, seeded with one consuming read of the output so far — `job_output` continues exactly after it. A kill from outside the call (the human stopping the job) settles the foreground result with `[stopped: <reason>]` ahead of the signal marker, so the model reads the reason instead of a command failure; cancelling the call itself kills the job. Registration is best-effort: `promoteOnTimeout: false`, a missing job registry, or a registry that refuses the job at its start (the owner's job limit, no controller) run the command under the executor's deadline kill instead, and the tool description advertises the hand-over only when it holds.
60
65
 
61
66
  ### Sandboxed execution and escalation
62
67
 
63
- When the mounted executor confines commands (for example `dsh-bash-sandbox`), a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a command failure. The model may then retry the exact same command once in the same turn with `sandbox_permissions` (the narrowest wider mode that suffices) and a one-sentence `justification`; the approval prompt raised by that retry is how the user consents. Request wider access only after a real denial; a rejected escalation is final for that command. Repeating the current mode runs without approval, while a narrower target fails before execution.
68
+ When the mounted executor confines commands (for example `dsh-bash-sandbox`), a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a command failure. The model may then retry the exact same command once in the same turn with `sandbox_permissions` (the narrowest wider mode that suffices) and a one-sentence `justification`; the approval prompt raised by that retry is how the user consents. Request wider access only after a real denial; a rejected escalation is final for that command. Repeating the current mode runs without approval, while a narrower target fails before execution. Without `sandbox_permissions`, `justification` may be omitted, empty, or whitespace-only; a non-empty reason without a mode is rejected. Repeating the effective mode also permits an omitted or blank reason. A different requested mode requires a non-empty reason; widening still requires approval.
64
69
 
65
70
  ### What can go wrong
66
71
 
@@ -81,14 +86,14 @@ This section explains the design decisions behind the tool and points at the cod
81
86
  - **Model-facing consumer of the shell seam.** The tool is the Consumer role of the bash capability: it registers the `bash` schema, renders results, and resolves per-call policy, while the executor seam owns process mechanics.
82
87
  - **Request from named args only.** The tool never exposes `stdin`, `env`, or `stdoutMaxBytes`; it builds each request from command/workdir/timeout/signal fields plus the registry-collected `dshEnv`, so model-supplied keys cannot replace managed values.
83
88
  - **Non-zero exits are reported, not errored.** Only infrastructure failures (spawn errors, aborts) surface as tool errors; the model interprets exit codes and markers.
84
- - **Background work belongs to the job runtime.** A background call registers a process handle with `ctx.jobs`; ids, ownership, completion notices, and disposal are the runtime's, and this tool only maps bash exit and sandbox facts into job output.
89
+ - **Every command belongs to the job runtime when one is composed.** A call registers its process handle with `ctx.jobs` as it starts, whether the model asked for the background or the tool waits on it; ids, ownership, completion notices, and disposal are the runtime's, and this tool only maps bash exit and sandbox facts into job output. Without a registry the tool is foreground-only, and it swaps between the two registrations as the registry comes and goes.
85
90
 
86
91
  ### Source map
87
92
 
88
93
  | File | Role |
89
94
  |---|---|
90
95
  | [`src/index.ts`](src/index.ts) | Plugin entry: tool registration, prompt section, arg validation, escalation, request assembly |
91
- | [`src/background.ts`](src/background.ts) | Own asynchronous shell preparation and map process settlement onto job outcomes |
96
+ | [`src/background.ts`](src/background.ts) | Own asynchronous shell preparation, map process settlement onto job outcomes, and render a ring read as a process read |
92
97
  | [`src/render.ts`](src/render.ts) | Model-facing result text: streams, markers, truncation notices |
93
98
  | — | No runtime invariant companion is published; the environment registry validates ownership and collected values at each mutation/read; it publishes no independent snapshot that a companion could cross-check. |
94
99
 
@@ -146,7 +151,7 @@ Prefix-stable while the registration scope and prompt text are unchanged. Plugin
146
151
 
147
152
  #### What the model sees
148
153
 
149
- The model sees the generated [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash). `run_in_background` appears only when this producer enables it; `sandbox_permissions` and `justification` appear only when the mounted executor advertises sandboxing. Agent-scoped tool restrictions can remove the definition for that agent.
154
+ The model sees the generated [`bash` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-bash). `run_in_background` appears only when this producer enables it and a job registry is composed; `sandbox_permissions` and `justification` appear only when the mounted executor advertises sandboxing. Agent-scoped tool restrictions can remove the definition for that agent.
150
155
 
151
156
  #### Token effect
152
157
 
@@ -160,7 +165,7 @@ Prefix-stable while visibility, background support, and executor sandbox capabil
160
165
 
161
166
  #### What the model sees
162
167
 
163
- The renderer emits the data-dependent stdout tail, then optional `[stderr]` and the stderr tail. With no output it emits exactly `(no output)`. Conditional lines are exactly `[output truncated; full output: <path-or-(unavailable)>]`, `[sandbox: file access denied under <mode> mode]`, `[timed out after <timeoutMs>ms]`, `[killed by signal: <signal>]`, and `[exit code: <exitCode>]`; the sandbox escalation and runner-failure lines are quoted in [`dsh-bash-sandbox`](../bash-sandbox/README.md).
168
+ The renderer emits the data-dependent stdout tail, then optional `[stderr]` and the stderr tail. With no output it emits exactly `(no output)`. Conditional lines are exactly `[output truncated; full output: <path-or-(unavailable)>]`, `[sandbox: file access denied under <mode> mode]`, `[timed out after <timeoutMs>ms]`, `[stopped: <reason>]`, `[killed by signal: <signal>]`, and `[exit code: <exitCode>]`; the sandbox escalation and runner-failure lines are quoted in [`dsh-bash-sandbox`](../bash-sandbox/README.md).
164
169
 
165
170
  #### Token effect
166
171
 
@@ -207,7 +212,8 @@ These limits define when the tool is a poor fit or needs special care. They are
207
212
 
208
213
  - **Replay exit pills parse from result text** — output whose final line happens to be exactly `[exit code: N]` / `[killed by signal: …]` shows a wrong pill on session replay and loses that line from the card body, because the parse treats it as the marker it consumes; a display-only known residual.
209
214
  - **The `bash` tool opts out of `timeout-policy` budgets** — it keeps the executor-owned `BASH_TIMEOUT` path, per [the tool-call timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md).
210
- - **Background processes have no executor timeout** — callers must use `job_kill`, or rely on owner/service disposal, when work no longer matters.
215
+ - **Background processes have no executor timeout** — callers must use `job_kill`, or rely on owner/service disposal, when work no longer matters; a foreground command registered as a job has none either, since its timeout bounds only the wait.
216
+ - **The job list shows every foreground command while it runs** — a settled one leaves with its result, but the Web task list does not yet mark which running rows a tool call is still waiting on.
211
217
 
212
218
  <a id="dev-note"></a>
213
219
  ### Dev Note
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-tool-bash` agent(智能体)运行一次性 `bash` 命令,并接收 stdout、stderr 与退出标记。每次调用都使用全新 shell,因此 cwd、变量和函数不会保留;`run_in_background` 可启动长时间运行的工作,agent 能用 `job_output` 检查、用 `job_kill` 停止。命令会收到受管 `DSH_*` 环境;沙箱拒绝后,可携带更宽的 `sandbox_permissions`、一句 `justification` 并经用户批准重试一次。非零退出会作为结果报告,因此由 agent 决定如何响应;请使用 `dsh-bash-local` 或 `dsh-bash-sandbox` 等执行器,并加载 `dsh-shell-env`。
12
+ `dsh-tool-bash` 运行 Bash 命令并返回 stdout、stderr 与退出标记。每次调用使用全新 shell;cwd、变量和函数不会保留。组合中有 job 注册表时,每条命令从启动起就是一个 job:`run_in_background` 立即返回 id,超过超时仍在运行的前台命令返回同一个 id,`job_output`/`job_kill` 收集并停止它。命令获得受管 `DSH_*` 环境;沙箱拒绝可携带更宽的 `sandbox_permissions`、一句 `justification` 与用户批准重试一次。非零退出是供 agent(智能体)解读的结果。请将 `dsh-bash-local` 或 `dsh-bash-sandbox` 等执行器与 `dsh-shell-env` 一起挂载。
13
13
 
14
14
  ## 目录
15
15
 
@@ -41,11 +41,12 @@ kind: "package-reference"
41
41
  - name: '@deepseek-ai/dsh-tool-jobs'
42
42
  ```
43
43
 
44
- 唯一的配置字段用于开关后台支持。
44
+ 配置字段决定后台能力面。
45
45
 
46
46
  | 字段 | 默认值 | 含义 |
47
47
  |---|---|---|
48
- | `enableRunInBackground` | `true` | 暴露 `run_in_background`;为 `false` 时拒绝强制后台调用 |
48
+ | `enableRunInBackground` | `true` | 组合中有 job 注册表时暴露 `run_in_background`;为 `false` 时拒绝强制后台调用 |
49
+ | `promoteOnTimeout` | `true` | 到达超时的前台命令继续作为它的后台任务运行,而不是杀掉它 |
49
50
 
50
51
  生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-bash)是每个受支持字段及其 JSDoc 的穷尽式真源;生成的[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash)携带完整参数 schema。
51
52
 
@@ -56,11 +57,15 @@ kind: "package-reference"
56
57
  <a id="running-long-commands-in-the-background"></a>
57
58
  ### 后台运行长时间命令
58
59
 
59
- 传入 `run_in_background: true` 会准入任务并立即返回 job id;限制准备可能仍在进行,且不设后台执行超时。进程可用前输出为空。任务取消会中止准备并停止随后返回的进程;启动失败使已准入任务以失败状态结算。agent 用 `job_output` 读取输出(除非 `wait: true`,否则非阻塞)、用 `job_list` 列出任务、用 `job_kill` 停止任务;完成的任务会在会话内通知拥有它的 agent。后台支持需要挂载通用任务运行时(`dsh-jobs-local`)及其控制工具(`dsh-tool-jobs`)。
60
+ 传入 `run_in_background: true` 会准入任务并立即返回 job id;限制准备可能仍在进行,且不设后台执行超时。进程可用前输出为空。任务取消会中止准备并停止随后返回的进程;启动失败使已准入任务以失败状态结算。agent 用 `job_output` 读取输出(除非 `wait: true`,否则非阻塞)、用 `job_list` 列出任务、用 `job_kill` 停止任务;完成的任务会在会话内通知拥有它的 agent。后台支持需要挂载通用任务运行时(`dsh-jobs-local`)及其控制工具(`dsh-tool-jobs`)。后台 job 把运行的非消费 `observed` 读取器交给 job 注册表作为拉式源;注册表按自己的节奏(`dsh-jobs-local` 的 `pumpPollMs`)把它们泵入 job 的输出环,Web 客户端由此流式看到实时输出,模型的 `job_output` 读取则经另一个游标消费同一份字节。读取器抛错只记录一次,该流就此停止,job 继续跑到自己的结算。输出环是尽力而为的实时预览:stdout 与 stderr 按轮询轮次复制,同一个轮询窗口内两条流的写入会先 stdout 后 stderr 出现,而不是按写入顺序。
61
+
62
+ ### 前台命令即任务
63
+
64
+ 组合中有 job 注册表时,前台命令一启动就登记到 `ctx.jobs`,调用等待该任务:命令在运行期间始终被列出、经 `job.list` 与 `job.follow` 流式观看,并可从 Web 任务列表停止。在超时内完成的命令返回普通前台结果,其任务记录随结果一起离开注册表,模型从不看到 id。超过超时仍在运行的命令继续作为它本来就是的那个任务运行,调用返回 `[still running after <timeoutMs>ms; moved to background job <id>]` 加任务交接指引,并以一次消费式读取带上目前为止的输出——`job_output` 恰好从此处接续。来自调用之外的杀停(人在界面上停止任务)会让前台结果在信号标记之前带上 `[stopped: <reason>]`,模型读到的是原因而不是命令失败;取消调用本身则杀掉任务。登记是尽力而为的:`promoteOnTimeout: false`、缺少 job 注册表,或注册表在启动时拒绝该任务(持有者的任务上限、没有控制器)都会改为在执行器的 deadline 杀下运行命令,工具描述也只在交接语义成立时才宣传它。
60
65
 
61
66
  ### 沙箱执行与升权
62
67
 
63
- 当已挂载的执行器约束命令(例如 `dsh-bash-sandbox`)时,被阻止的文件操作会报告为 `[sandbox: file access denied under <mode> mode]`——这是策略拒绝,不是命令失败。模型随后可以在同一轮次中用 `sandbox_permissions`(满足需要的最窄更宽模式)与一句 `justification` 重试完全相同的命令一次;该重试引发的审批提示就是用户同意的方式。只有发生真实拒绝后才请求更宽权限;被拒绝的升权对该命令即为最终结果。重复当前模式无需审批即可执行,更窄目标则在执行前失败。
68
+ 当已挂载的执行器约束命令(例如 `dsh-bash-sandbox`)时,被阻止的文件操作会报告为 `[sandbox: file access denied under <mode> mode]`——这是策略拒绝,不是命令失败。模型随后可以在同一轮次中用 `sandbox_permissions`(满足需要的最窄更宽模式)与一句 `justification` 重试完全相同的命令一次;该重试引发的审批提示就是用户同意的方式。只有发生真实拒绝后才请求更宽权限;被拒绝的升权对该命令即为最终结果。重复当前模式无需审批即可执行,更窄目标则在执行前失败。未提供 `sandbox_permissions` 时,`justification` 可以省略、为空字符串或仅含空白;未指定模式却提供非空理由会被拒绝。重复当前生效模式时也可省略理由或提供空白理由。请求不同模式时必须提供非空理由;升权仍需审批。
64
69
 
65
70
  ### 可能出什么问题
66
71
 
@@ -81,14 +86,14 @@ kind: "package-reference"
81
86
  - **shell seam 的模型侧消费方。** 本工具是 bash 能力的消费方角色:它注册 `bash` schema、渲染结果并解析每次调用的策略,进程机制归执行器 seam 所有。
82
87
  - **请求只来自命名参数。** 工具从不暴露 `stdin`、`env` 或 `stdoutMaxBytes`;它只用命令/workdir/超时/信号字段加上注册表收集的 `dshEnv` 构建每个请求,因此模型提供的键无法替换受管值。
83
88
  - **非零退出只报告、不失败。** 只有基础设施故障(spawn 错误、中止)才会作为工具错误暴露;模型解读退出码与标记。
84
- - **后台工作归任务运行时。** 后台调用把进程句柄注册到 `ctx.jobs`;job id、所有权、完成通知与释放都是运行时的职责,本工具只把 bash 退出与沙箱事实映射为任务输出。
89
+ - **组合了任务运行时,每条命令都归它。** 调用一启动就把进程句柄注册到 `ctx.jobs`,无论是模型要求后台还是工具在等待它;job id、所有权、完成通知与释放都是运行时的职责,本工具只把 bash 退出与沙箱事实映射为任务输出。没有注册表时工具只做前台,并随注册表的出现与消失在两种注册之间切换。
85
90
 
86
91
  ### 源码地图
87
92
 
88
93
  | 文件 | 职责 |
89
94
  |---|---|
90
95
  | [`src/index.ts`](src/index.ts) | 插件入口:工具注册、提示词区段、参数校验、升权、请求组装 |
91
- | [`src/background.ts`](src/background.ts) | 管理异步 shell 准备,并将进程结算映射为任务结果 |
96
+ | [`src/background.ts`](src/background.ts) | 管理异步 shell 准备、将进程结算映射为任务结果,并把输出环读取渲染为进程读取 |
92
97
  | [`src/render.ts`](src/render.ts) | 模型侧结果文本:流、标记、截断通知 |
93
98
  | — | 不发布运行时不变式伴生入口;环境注册表在每次变更和读取时校验所有权及收集值,且不发布可供伴生入口交叉核对的独立快照;执行关系由能力 seam 负责。 |
94
99
 
@@ -146,7 +151,7 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
146
151
 
147
152
  #### 模型看到什么
148
153
 
149
- 模型会看到生成的 [`bash` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash)。仅当本生产方启用 `run_in_background` 时,该字段才会出现;仅当已挂载执行器声明支持沙箱时,`sandbox_permissions` 和 `justification` 才会出现。按 agent 作用域限制工具可以移除该 agent 的定义。
154
+ 模型会看到生成的 [`bash` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-bash)。仅当本生产方启用 `run_in_background` 且组合中有 job 注册表时,该字段才会出现;仅当已挂载执行器声明支持沙箱时,`sandbox_permissions` 和 `justification` 才会出现。按 agent 作用域限制工具可以移除该 agent 的定义。
150
155
 
151
156
  #### Token 影响
152
157
 
@@ -160,7 +165,7 @@ Check the [exit code: N] marker on every bash result; investigate failures befor
160
165
 
161
166
  #### 模型看到什么
162
167
 
163
- renderer 输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]` 和 stderr 尾部。没有输出时,它精确输出 `(no output)`。条件行精确为 `[output truncated; full output: <path-or-(unavailable)>]`、`[sandbox: file access denied under <mode> mode]`、`[timed out after <timeoutMs>ms]`、`[killed by signal: <signal>]` 与 `[exit code: <exitCode>]`;沙箱升权与 runner 故障行原文列于 [`dsh-bash-sandbox`](../bash-sandbox/README.zh.md)。
168
+ renderer 输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]` 和 stderr 尾部。没有输出时,它精确输出 `(no output)`。条件行精确为 `[output truncated; full output: <path-or-(unavailable)>]`、`[sandbox: file access denied under <mode> mode]`、`[timed out after <timeoutMs>ms]`、`[stopped: <reason>]`、`[killed by signal: <signal>]` 与 `[exit code: <exitCode>]`;沙箱升权与 runner 故障行原文列于 [`dsh-bash-sandbox`](../bash-sandbox/README.zh.md)。
164
169
 
165
170
  #### Token 影响
166
171
 
@@ -207,7 +212,8 @@ renderer 输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]`
207
212
 
208
213
  - **回放的退出 pill 从结果文本解析**——输出最后一行恰好是 `[exit code: N]` / `[killed by signal: …]` 时,会话回放会显示错误的 pill 并从卡片正文丢失该行,因为解析把它当作要消费的标记;这是仅影响显示的已知残留。
209
214
  - **`bash` 工具不参与 `timeout-policy` 预算**——它保留执行器自有的 `BASH_TIMEOUT` 路径,见[工具调用超时策略 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md)。
210
- - **后台进程没有执行器超时**——工作不再需要时,调用方必须使用 `job_kill`,或依赖持有者/服务的释放。
215
+ - **后台进程没有执行器超时**——工作不再需要时,调用方必须使用 `job_kill`,或依赖持有者/服务的释放;登记为任务的前台命令同样没有,它的超时只限定等待时长。
216
+ - **任务列表会显示每条运行中的前台命令**——完成的命令随结果离开,但 Web 任务列表尚未标出哪些运行中的行仍有工具调用在等待。
211
217
 
212
218
  <a id="dev-note"></a>
213
219
  ### 开发备注
package/lib/index.js CHANGED
@@ -6,35 +6,98 @@ import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDen
6
6
  import { DSH_ENV_PREFIX, parseExitStatus } from "@deepseek-ai/dsh-shell";
7
7
  //#region lib/types/background.js
8
8
  /**
9
- * Generic-task adaptation for background bash process handles.
9
+ * Generic-job adaptation for bash process handles: the terminal outcome the
10
+ * registry records, the pull sources it pumps, and the ring read a foreground
11
+ * call renders when it stops waiting.
10
12
  *
11
13
  * @module @deepseek-ai/dsh-tool-bash/background
12
14
  */
13
15
  /**
14
- * Map a settled background process onto the generic task-outcome vocabulary:
16
+ * Sandbox facts worth the terminal detail: a runner that never ran the
17
+ * command, or a denial (with the escalation hint this composition offers).
18
+ * @param sandbox - settled sandbox facts, when this was a confined process.
19
+ * @param escalationModes - escalation targets advertised by this composition.
20
+ * @returns the markers to append, oldest first.
21
+ */
22
+ function sandboxNotes(sandbox, escalationModes) {
23
+ if (sandbox?.runnerFailed) return [`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`];
24
+ if (sandbox?.denied) {
25
+ const notes = [sandboxDenialMarker(sandbox.mode)];
26
+ if (escalationModes.length > 0) notes.push(escalationHintMarker("command"));
27
+ return notes;
28
+ }
29
+ return [];
30
+ }
31
+ /**
32
+ * Map a settled background process onto the generic job-outcome vocabulary:
15
33
  * `killed` stays `killed` (detail: the signal when one is known), everything
16
34
  * else is `completed` with the exit code as detail. A nonzero command exit is
17
- * reported, not failed, exactly like the foreground rendering.
35
+ * reported, not failed, exactly like the foreground rendering. Sandbox facts
36
+ * join the detail, since a job's terminal reason is the one line every
37
+ * reader — the model's status line, the roster row — shows.
18
38
  * @param proc - the settled process handle.
39
+ * @param escalationModes - escalation targets advertised by this composition.
19
40
  * @returns the outcome for the `ctx.jobs` registration.
20
41
  */
21
- function processOutcome(proc) {
22
- if (proc.status === "killed") return {
42
+ function processOutcome(proc, escalationModes = []) {
43
+ const base = proc.status === "killed" ? {
23
44
  status: "killed",
24
45
  detail: proc.signal !== null ? `signal: ${proc.signal}` : "killed before exit"
25
- };
26
- return {
46
+ } : {
27
47
  status: "completed",
28
48
  detail: `exit code: ${proc.exitCode ?? 0}`
29
49
  };
50
+ const notes = sandboxNotes(proc.sandbox, escalationModes);
51
+ return notes.length === 0 ? base : {
52
+ ...base,
53
+ detail: `${base.detail}; ${notes.join(" ")}`
54
+ };
55
+ }
56
+ /**
57
+ * The process's non-consuming stream readers as registry pull sources. They
58
+ * bind lazily because the process is spawned inside the starter, after the
59
+ * registry admitted the job; a read before the spawn yields nothing, and the
60
+ * pump keeps the model's consuming cursor untouched. A rejected spawn's
61
+ * stderr reader carries the provider's `subprocess failed before reporting an
62
+ * outcome: …` note.
63
+ * @param proc - the started process's observed streams, once the starter has spawned it.
64
+ * @returns one source per stream, stdout first.
65
+ */
66
+ function processSources(proc) {
67
+ const source = (channel) => ({
68
+ channel,
69
+ read: (fromByte) => {
70
+ const live = proc();
71
+ return live === void 0 ? {
72
+ text: "",
73
+ nextOffset: fromByte,
74
+ lossy: false
75
+ } : live.observed[channel].readFrom(fromByte);
76
+ }
77
+ });
78
+ return [source("stdout"), source("stderr")];
79
+ }
80
+ /**
81
+ * The ring chunks of one consuming registry read as the shell tools render a
82
+ * process read: stdout chunks in order, then every stderr chunk in one
83
+ * `[stderr]` section, so the output a foreground call hands over when it
84
+ * stops waiting reads exactly like the `job_output` reads that follow it.
85
+ * @param chunks - the chunks since the model cursor, in offset order.
86
+ * @returns the delta text, possibly empty.
87
+ */
88
+ function ringDelta(chunks) {
89
+ const out = chunks.filter((chunk) => chunk.channel !== "stderr").map((chunk) => chunk.text).join("");
90
+ const err = chunks.filter((chunk) => chunk.channel === "stderr").map((chunk) => chunk.text).join("");
91
+ const separator = out.length > 0 && !out.endsWith("\n") ? "\n" : "";
92
+ return out + (err.length > 0 ? `${separator}[stderr]\n${err}` : "");
30
93
  }
31
94
  /**
32
95
  * Adapt asynchronous shell preparation after job admission without exposing a partial process.
33
96
  * @param start - starts the process with job-owned cancellation.
34
- * @param renderOutput - consumes output from a published process.
97
+ * @param outcome - projects the settled process into the job outcome.
35
98
  * @returns synchronous job hooks whose completion includes preparation and process settlement.
36
99
  */
37
- function processJob(start, renderOutput) {
100
+ function processJob(start, outcome) {
38
101
  const controller = new AbortController();
39
102
  let process;
40
103
  return {
@@ -51,15 +114,14 @@ function processJob(start, renderOutput) {
51
114
  } finally {
52
115
  await process.done;
53
116
  }
54
- return processOutcome(process);
117
+ return outcome(process);
55
118
  } catch (error) {
56
119
  return {
57
120
  status: controller.signal.aborted && process === void 0 ? "killed" : "failed",
58
121
  detail: error instanceof Error ? error.message : String(error)
59
122
  };
60
123
  }
61
- })(),
62
- readOutput: () => process === void 0 ? "" : renderOutput(process)
124
+ })()
63
125
  };
64
126
  }
65
127
  //#endregion
@@ -83,7 +145,7 @@ function streamText(output) {
83
145
  * @param escalationModes - the escalation targets this composition advertises;
84
146
  * non-empty adds the same-turn escalation hint after a denial marker
85
147
  * (default `[]`: no hint).
86
- * @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
148
+ * @returns the model-facing text: output body (or `(no output)`), then any timeout/stopped/signal/exit markers, each on its own line.
87
149
  */
88
150
  function renderResult(result, escalationModes = []) {
89
151
  const out = streamText(result.stdout);
@@ -100,6 +162,7 @@ function renderResult(result, escalationModes = []) {
100
162
  if (escalationModes.length > 0) markers.push(escalationHintMarker("command"));
101
163
  }
102
164
  if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`);
165
+ if (result.stopped !== void 0) markers.push(`[stopped: ${result.stopped}]`);
103
166
  if (result.signal !== null) markers.push(`[killed by signal: ${result.signal}]`);
104
167
  else if (result.exitCode !== 0) markers.push(`[exit code: ${result.exitCode}]`);
105
168
  if (markers.length === 0) return body;
@@ -107,35 +170,50 @@ function renderResult(result, escalationModes = []) {
107
170
  return body + markers.join("\n");
108
171
  }
109
172
  /**
110
- * Shape one background-process read into the `job_output` delta the model
111
- * sees: the incremental delta, plus the lossy-read notice (with full-stream
112
- * spill paths) when in-memory truncation dropped unread bytes. Empty-delta
113
- * rendering (`(no new output)`) is the generic job controller's job.
114
- * @param read - one incremental read from the process handle.
173
+ * Shape a foreground call that stopped waiting into the text the model sees:
174
+ * the output captured so far (one consuming registry read taken at that
175
+ * point, so `job_output` continues exactly after it), then the still-running
176
+ * marker and the job hand-off guidance.
177
+ * @param promoted - the promoted result value: the job id, the wait that
178
+ * expired, and the output so far.
179
+ * @returns the model-facing text for a promoted call.
180
+ */
181
+ function renderPromoted(promoted) {
182
+ return `${promoted.output.length > 0 ? promoted.output.endsWith("\n") ? promoted.output : `${promoted.output}\n` : ""}[still running after ${promoted.timeoutMs}ms; moved to background job ${promoted.jobId}]\nThe command keeps running in the background. You will be notified when it finishes; read newer output with job_output, stop it with job_kill.`;
183
+ }
184
+ /**
185
+ * Shape the one consuming registry read a foreground call embeds in its
186
+ * result when it stops waiting: the output produced so far, plus the
187
+ * dropped-output notice (naming the job's spill files) when the model cursor
188
+ * fell behind the ring, and the sandbox notices. Later `job_output` reads
189
+ * render the same ring through the job tools.
190
+ * @param delta - the read's chunks as rendered text.
191
+ * @param lossy - whether bytes before the delta were evicted unread.
192
+ * @param spillPaths - the complete-stream files the job currently advertises.
115
193
  * @param sandbox - settled sandbox facts, when this was a confined process.
116
194
  * @param escalationModes - escalation targets advertised by this composition.
117
195
  * @returns the delta text with any loss or sandbox notice appended.
118
196
  */
119
- function renderProcessRead(read, sandbox, escalationModes = []) {
197
+ function renderJobRead(delta, lossy, spillPaths, sandbox, escalationModes = []) {
120
198
  const notices = [];
121
- if (read.lossy) {
122
- const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
123
- notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
124
- }
199
+ if (lossy) notices.push(`[some output was dropped from memory; full output: ${spillPaths.length > 0 ? spillPaths.join(", ") : "(unavailable)"}]`);
125
200
  if (sandbox?.runnerFailed) notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`);
126
201
  else if (sandbox?.denied) {
127
202
  notices.push(sandboxDenialMarker(sandbox.mode));
128
203
  if (escalationModes.length > 0) notices.push(escalationHintMarker("command"));
129
204
  }
130
- if (notices.length === 0) return read.delta;
131
- return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
205
+ if (notices.length === 0) return delta;
206
+ return `${delta}${delta.length > 0 && !delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
132
207
  }
133
208
  //#endregion
134
209
  //#region lib/types/index.js
135
210
  /**
136
- * Model-facing Consumer of the `ctx.shell` capability seam. Background calls
137
- * register process handles with `ctx.jobs`; their work uses job cancellation
138
- * rather than the tool-call signal after an id is returned.
211
+ * Model-facing Consumer of the `ctx.shell` capability seam. While a job
212
+ * registry is composed, every call registers its process with `ctx.jobs` as
213
+ * it starts: `run_in_background` returns the id at once, and a foreground call
214
+ * waits on its job until the command finishes or the wait times out, at which
215
+ * point it returns the same id. Without a registry the tool is foreground-only
216
+ * and the executor's deadline kills the command.
139
217
  *
140
218
  * TODO(permissions): deployment policy belongs in `tools/pre-execute` and
141
219
  * sandboxing executors; see docs/architecture.md § Where new behavior goes.
@@ -149,15 +227,20 @@ const inject = [
149
227
  "shellEnv"
150
228
  ];
151
229
  /** Runtime configuration schema for the bash tool plugin. */
152
- const Config = z.object({ enableRunInBackground: z.boolean().default(true) });
153
- function validateBashArgs(args) {
230
+ const Config = z.object({
231
+ enableRunInBackground: z.boolean().default(true),
232
+ promoteOnTimeout: z.boolean().default(true)
233
+ });
234
+ function validateBashArgs(args, effectiveMode) {
154
235
  if (args.command.trim().length === 0) throw new Error("invalid command: expected a non-empty string");
155
236
  if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
156
237
  if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`);
157
- validateEscalationArgs(args.sandbox_permissions, args.justification);
238
+ if (args.sandbox_permissions !== void 0 && args.sandbox_permissions === effectiveMode) return;
239
+ const justification = args.sandbox_permissions === void 0 && args.justification?.trim() === "" ? void 0 : args.justification;
240
+ validateEscalationArgs(args.sandbox_permissions, justification);
158
241
  }
159
- function bashDescription(backgroundEnabled, escalationModes) {
160
- const background = backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." : "Background execution is not available; long-running commands must finish within the timeout.";
242
+ function bashDescription(backgroundEnabled, escalationModes, promoteOnTimeout) {
243
+ const background = backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." + (promoteOnTimeout ? " A foreground command that reaches its timeout is not killed: it moves to the background the same way, returning its job id and the output so far." : "") : "Background execution is not available; long-running commands must finish within the timeout.";
161
244
  const base = `Execute a bash command (\`bash -c\`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass \`workdir\` instead of using \`cd\`. Non-zero exits are reported as \`[exit code: N]\`. Current harness environment facts are exposed through managed \`$${DSH_ENV_PREFIX}*\` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as \`[sandbox: file access denied under <mode> mode]\` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. ` + background;
162
245
  if (escalationModes.length === 0) return base;
163
246
  return base + " Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.";
@@ -188,7 +271,9 @@ function presentBashResult(args, result) {
188
271
  const block = result.content.length === 1 ? result.content[0] : void 0;
189
272
  if (block === void 0 || block.type !== "text") return void 0;
190
273
  const raw = block.text;
191
- if (typeof args === "object" && args !== null && args.run_in_background === true || result.isError) return {
274
+ const isBackground = typeof args === "object" && args !== null && args.run_in_background === true;
275
+ const isPromoted = result.value?.kind === "promoted";
276
+ if (isBackground || isPromoted || result.isError) return {
192
277
  card: "generic",
193
278
  content: [{
194
279
  type: "text",
@@ -238,6 +323,12 @@ function canonicalBashResult(result) {
238
323
  } } : {}
239
324
  };
240
325
  }
326
+ /** The structured abort the foreground paths throw when the caller cancels the call. */
327
+ function toolAborted() {
328
+ const error = new HarnessError("tool call aborted", TOOL_ABORTED);
329
+ error.name = "AbortError";
330
+ return error;
331
+ }
241
332
  /** Canonical background-handle properties shared by the bash output union. */
242
333
  const BACKGROUND_OUTPUT_PROPERTIES = {
243
334
  kind: {
@@ -252,6 +343,7 @@ const BACKGROUND_OUTPUT_PROPERTIES = {
252
343
  };
253
344
  function apply(ctx, config = {}) {
254
345
  const backgroundEnabled = config.enableRunInBackground ?? true;
346
+ const promoteOnTimeout = (config.promoteOnTimeout ?? true) && backgroundEnabled;
255
347
  const defaultMode = ctx.shell.sandboxMode;
256
348
  const escalationModes = defaultMode === void 0 ? [] : ESCALATION_TARGETS;
257
349
  const sandboxPolicy = defaultMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
@@ -290,189 +382,335 @@ function apply(ctx, config = {}) {
290
382
  order: ctx.systemPrompt.getSectionOrder("TOOL_BASH"),
291
383
  text: "Check the [exit code: N] marker on every bash result; investigate failures before moving on."
292
384
  });
293
- ctx.tools.register(defineTool({
294
- name: "bash",
295
- description: bashDescription(backgroundEnabled, escalationModes),
296
- parameters: {
297
- command: {
298
- type: "string",
299
- required: true,
300
- description: "The bash command to execute."
301
- },
302
- description: {
303
- type: "string",
304
- required: true,
305
- description: "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
306
- },
307
- timeoutMs: {
308
- type: "number",
309
- description: "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
310
- },
311
- workdir: {
312
- type: "string",
313
- description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
314
- },
315
- ...backgroundEnabled ? { run_in_background: {
316
- type: "boolean",
317
- description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
318
- } } : {},
319
- ...escalationModes.length > 0 ? {
320
- sandbox_permissions: {
385
+ /**
386
+ * One registration of the `bash` tool. With a registry, every call
387
+ * registers its process as a job at its start; without one the tool is
388
+ * foreground-only and the executor's deadline kills the command.
389
+ */
390
+ const bashTool = (jobs) => {
391
+ const background = jobs !== void 0;
392
+ const promote = background && promoteOnTimeout;
393
+ /** Register the command as a job; the process spawns inside the starter, after admission. */
394
+ const startJob = (registry, args, exec, spec) => {
395
+ let proc;
396
+ let stopped;
397
+ return {
398
+ id: registry.start({
399
+ kind: "bash",
400
+ label: args.command,
401
+ ...exec.agent ? { owner: exec.agent.id } : {},
402
+ output: processSources(() => proc),
403
+ run: () => {
404
+ const hooks = processJob(async (signal) => {
405
+ proc = await ctx.shell.execute({
406
+ ...spec,
407
+ signal
408
+ });
409
+ return proc;
410
+ }, (started) => processOutcome(started, escalationModes));
411
+ return {
412
+ done: hooks.done,
413
+ cancel: (reason) => {
414
+ stopped = reason;
415
+ hooks.cancel(reason);
416
+ }
417
+ };
418
+ }
419
+ }),
420
+ process: () => proc,
421
+ stopped: () => stopped
422
+ };
423
+ };
424
+ /** Wait on a registered foreground command until it settles or the timeout passes. */
425
+ const waitOnJob = async (registry, attached, exec, spec) => {
426
+ const owner = exec.agent?.id;
427
+ const timeoutMs = spec.timeoutMs;
428
+ /**
429
+ * Stop the job on this call's own account and stay on it until it
430
+ * settles, so the settlement is `awaited` and no completion notice
431
+ * follows a result this call already carries; the record then leaves
432
+ * with the call, as the model never saw the id.
433
+ */
434
+ const stop = async (reason) => {
435
+ registry.kill(attached.id, owner, reason);
436
+ const settled = await registry.wait(attached.id, timeoutMs, owner);
437
+ if (settled.status !== "running" && settled.status !== "stopping") registry.remove(attached.id, owner);
438
+ return settled;
439
+ };
440
+ let view;
441
+ try {
442
+ view = await registry.wait(attached.id, timeoutMs, owner, exec.signal);
443
+ } catch {
444
+ await stop("tool call aborted");
445
+ throw toolAborted();
446
+ }
447
+ if ((view.status === "running" || view.status === "stopping") && attached.process() === void 0) {
448
+ await stop("timed out during preparation");
449
+ return {
450
+ kind: "foreground",
451
+ exitCode: null,
452
+ signal: null,
453
+ timedOut: true,
454
+ aborted: false,
455
+ timeoutMs,
456
+ stdout: {
457
+ text: "",
458
+ truncated: false
459
+ },
460
+ stderr: {
461
+ text: "",
462
+ truncated: false
463
+ },
464
+ ...spec.sandboxPolicy !== void 0 ? { sandbox: {
465
+ mode: spec.sandboxPolicy.mode,
466
+ denied: false
467
+ } } : {}
468
+ };
469
+ }
470
+ if (view.status === "running" || view.status === "stopping") {
471
+ const read = registry.read(attached.id, owner);
472
+ return {
473
+ kind: "promoted",
474
+ jobId: attached.id,
475
+ timeoutMs,
476
+ output: renderJobRead(ringDelta(read.chunks), read.lossy, read.job.output.spillPaths ?? [], attached.process()?.sandbox, escalationModes)
477
+ };
478
+ }
479
+ registry.remove(attached.id, owner);
480
+ const process = attached.process();
481
+ if (process === void 0) throw new Error(view.detail);
482
+ const result = await process.result();
483
+ const stopped = attached.stopped();
484
+ return {
485
+ kind: "foreground",
486
+ ...canonicalBashResult(result),
487
+ ...stopped !== void 0 ? { stopped } : {}
488
+ };
489
+ };
490
+ return defineTool({
491
+ name: "bash",
492
+ description: bashDescription(background, escalationModes, promote),
493
+ parameters: {
494
+ command: {
321
495
  type: "string",
322
- enum: [...escalationModes],
323
- description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
496
+ required: true,
497
+ description: "The bash command to execute."
324
498
  },
325
- justification: {
499
+ description: {
326
500
  type: "string",
327
- description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
328
- }
329
- } : {}
330
- },
331
- output: {
332
- schema: { oneOf: [{
333
- type: "object",
334
- additionalProperties: false,
335
- properties: BACKGROUND_OUTPUT_PROPERTIES
336
- }, {
337
- type: "object",
338
- additionalProperties: false,
339
- properties: {
340
- kind: {
501
+ required: true,
502
+ description: "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
503
+ },
504
+ timeoutMs: {
505
+ type: "number",
506
+ description: promote ? "Timeout in milliseconds. The executor applies its configured default and cap; on expiry the command moves to the background as a job instead of being killed." : "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
507
+ },
508
+ workdir: {
509
+ type: "string",
510
+ description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
511
+ },
512
+ ...background ? { run_in_background: {
513
+ type: "boolean",
514
+ description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
515
+ } } : {},
516
+ ...escalationModes.length > 0 ? {
517
+ sandbox_permissions: {
341
518
  type: "string",
342
- required: true,
343
- const: "foreground"
344
- },
345
- exitCode: {
346
- required: true,
347
- oneOf: [{ type: "integer" }, { type: "null" }]
348
- },
349
- signal: {
350
- required: true,
351
- oneOf: [{ type: "string" }, { type: "null" }]
352
- },
353
- timedOut: {
354
- type: "boolean",
355
- required: true
519
+ enum: [...escalationModes],
520
+ description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
356
521
  },
357
- aborted: {
358
- type: "boolean",
359
- required: true
360
- },
361
- timeoutMs: {
362
- type: "number",
363
- required: true
522
+ justification: {
523
+ type: "string",
524
+ description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
525
+ }
526
+ } : {}
527
+ },
528
+ output: {
529
+ schema: { oneOf: [
530
+ {
531
+ type: "object",
532
+ additionalProperties: false,
533
+ properties: BACKGROUND_OUTPUT_PROPERTIES
364
534
  },
365
- stdout: {
535
+ {
366
536
  type: "object",
367
537
  additionalProperties: false,
368
- required: true,
369
538
  properties: {
370
- text: {
539
+ kind: {
540
+ type: "string",
541
+ required: true,
542
+ const: "promoted"
543
+ },
544
+ jobId: {
371
545
  type: "string",
372
546
  required: true
373
547
  },
374
- truncated: {
375
- type: "boolean",
548
+ timeoutMs: {
549
+ type: "number",
376
550
  required: true
377
551
  },
378
- spillPath: { type: "string" }
552
+ output: {
553
+ type: "string",
554
+ required: true
555
+ }
379
556
  }
380
557
  },
381
- stderr: {
558
+ {
382
559
  type: "object",
383
560
  additionalProperties: false,
384
- required: true,
385
561
  properties: {
386
- text: {
562
+ kind: {
387
563
  type: "string",
388
- required: true
564
+ required: true,
565
+ const: "foreground"
566
+ },
567
+ exitCode: {
568
+ required: true,
569
+ oneOf: [{ type: "integer" }, { type: "null" }]
570
+ },
571
+ signal: {
572
+ required: true,
573
+ oneOf: [{ type: "string" }, { type: "null" }]
389
574
  },
390
- truncated: {
575
+ timedOut: {
391
576
  type: "boolean",
392
577
  required: true
393
578
  },
394
- spillPath: { type: "string" }
395
- }
396
- },
397
- sandbox: {
398
- type: "object",
399
- additionalProperties: false,
400
- properties: {
401
- mode: {
402
- type: "string",
579
+ aborted: {
580
+ type: "boolean",
403
581
  required: true
404
582
  },
405
- denied: {
406
- type: "boolean",
583
+ stopped: { type: "string" },
584
+ timeoutMs: {
585
+ type: "number",
407
586
  required: true
408
587
  },
409
- enforcement: { type: "string" },
410
- runnerFailed: { type: "boolean" }
588
+ stdout: {
589
+ type: "object",
590
+ additionalProperties: false,
591
+ required: true,
592
+ properties: {
593
+ text: {
594
+ type: "string",
595
+ required: true
596
+ },
597
+ truncated: {
598
+ type: "boolean",
599
+ required: true
600
+ },
601
+ spillPath: { type: "string" }
602
+ }
603
+ },
604
+ stderr: {
605
+ type: "object",
606
+ additionalProperties: false,
607
+ required: true,
608
+ properties: {
609
+ text: {
610
+ type: "string",
611
+ required: true
612
+ },
613
+ truncated: {
614
+ type: "boolean",
615
+ required: true
616
+ },
617
+ spillPath: { type: "string" }
618
+ }
619
+ },
620
+ sandbox: {
621
+ type: "object",
622
+ additionalProperties: false,
623
+ properties: {
624
+ mode: {
625
+ type: "string",
626
+ required: true
627
+ },
628
+ denied: {
629
+ type: "boolean",
630
+ required: true
631
+ },
632
+ enforcement: { type: "string" },
633
+ runnerFailed: { type: "boolean" }
634
+ }
635
+ }
411
636
  }
412
637
  }
638
+ ] },
639
+ render: (_args, value) => [{
640
+ type: "text",
641
+ text: value.kind === "background" ? `started background job ${value.jobId}` : value.kind === "promoted" ? renderPromoted(value) : renderResult(value, escalationModes)
642
+ }]
643
+ },
644
+ async execute(args, exec) {
645
+ const standingPolicy = resolveSandboxPolicy(exec);
646
+ validateBashArgs(args, standingPolicy?.mode);
647
+ const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approveBashEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
648
+ const policy = approvedMode === void 0 ? standingPolicy : {
649
+ ...standingPolicy,
650
+ mode: approvedMode
651
+ };
652
+ const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot);
653
+ const dshEnv = ctx.shellEnv.collect(exec);
654
+ const request = {
655
+ command: args.command,
656
+ ...workdir !== void 0 ? { workdir } : {},
657
+ ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
658
+ dshEnv,
659
+ ...policy !== void 0 ? { sandboxPolicy: policy } : {}
660
+ };
661
+ if (args.run_in_background === true) {
662
+ if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
663
+ if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
664
+ if (exec.signal.aborted) throw toolAborted();
665
+ return {
666
+ kind: "background",
667
+ jobId: startJob(jobs, args, exec, ctx.shell.resolve({
668
+ ...request,
669
+ onExpiry: "none"
670
+ })).id
671
+ };
413
672
  }
414
- }] },
415
- render: (_args, value) => [{
416
- type: "text",
417
- text: value.kind === "background" ? `started background job ${value.jobId}` : renderResult(value, escalationModes)
418
- }]
419
- },
420
- async execute(args, exec) {
421
- validateBashArgs(args);
422
- const standingPolicy = resolveSandboxPolicy(exec);
423
- const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approveBashEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
424
- const policy = approvedMode === void 0 ? standingPolicy : {
425
- ...standingPolicy,
426
- mode: approvedMode
427
- };
428
- const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot);
429
- const dshEnv = ctx.shellEnv.collect(exec);
430
- const request = {
431
- command: args.command,
432
- ...workdir !== void 0 ? { workdir } : {},
433
- ...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
434
- dshEnv,
435
- ...policy !== void 0 ? { sandboxPolicy: policy } : {}
436
- };
437
- if (args.run_in_background === true) {
438
- if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
439
- const jobs = ctx.get("jobs");
440
- if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
441
- if (exec.signal.aborted) {
442
- const error = new HarnessError("tool call aborted", TOOL_ABORTED);
443
- error.name = "AbortError";
444
- throw error;
673
+ if (jobs !== void 0 && promote) {
674
+ const spec = ctx.shell.resolve({
675
+ ...request,
676
+ onExpiry: "none"
677
+ });
678
+ let attached;
679
+ try {
680
+ attached = startJob(jobs, args, exec, spec);
681
+ } catch (error) {
682
+ ctx.logger.warn(`bash: job registration refused, running in the foreground with the timeout kill instead: ${String(error)}`);
683
+ }
684
+ if (attached !== void 0) return waitOnJob(jobs, attached, exec, spec);
445
685
  }
686
+ const result = await (await ctx.shell.execute(ctx.shell.resolve({
687
+ ...request,
688
+ signal: exec.signal
689
+ }))).result();
690
+ if (result.aborted) throw toolAborted();
446
691
  return {
447
- kind: "background",
448
- jobId: jobs.start({
449
- kind: "bash",
450
- label: args.command,
451
- ...exec.agent ? { owner: exec.agent } : {},
452
- run: () => processJob((signal) => ctx.shell.start(ctx.shell.resolve({
453
- ...request,
454
- signal
455
- })), (proc) => renderProcessRead(proc.readOutput(), proc.sandbox, escalationModes))
456
- })
692
+ kind: "foreground",
693
+ ...canonicalBashResult(result)
457
694
  };
458
- }
459
- const result = await ctx.shell.run(ctx.shell.resolve({
460
- ...request,
461
- signal: exec.signal
462
- }));
463
- if (result.aborted) {
464
- const error = new HarnessError("tool call aborted", TOOL_ABORTED);
465
- error.name = "AbortError";
466
- throw error;
467
- }
468
- return {
469
- kind: "foreground",
470
- ...canonicalBashResult(result)
471
- };
472
- },
473
- presentCall: presentBashCall,
474
- presentResult: presentBashResult
475
- }));
695
+ },
696
+ presentCall: presentBashCall,
697
+ presentResult: presentBashResult
698
+ });
699
+ };
700
+ if (!backgroundEnabled) {
701
+ ctx.tools.register(bashTool(void 0));
702
+ return;
703
+ }
704
+ let foregroundOnly = ctx.get("jobs") === void 0 ? ctx.tools.register(bashTool(void 0)) : void 0;
705
+ ctx.inject(["jobs"], (jobCtx) => {
706
+ foregroundOnly?.();
707
+ foregroundOnly = void 0;
708
+ const unregister = ctx.tools.register(bashTool(jobCtx.jobs));
709
+ jobCtx.effect(() => () => {
710
+ unregister();
711
+ if (ctx.fiber.state === 2) foregroundOnly = ctx.tools.register(bashTool(void 0));
712
+ });
713
+ });
476
714
  }
477
715
  //#endregion
478
716
  export { Config, apply, inject, name };
@@ -1,27 +1,50 @@
1
1
  /**
2
- * Generic-task adaptation for background bash process handles.
2
+ * Generic-job adaptation for bash process handles: the terminal outcome the
3
+ * registry records, the pull sources it pumps, and the ring read a foreground
4
+ * call renders when it stops waiting.
3
5
  *
4
6
  * @module @deepseek-ai/dsh-tool-bash/background
5
7
  */
8
+ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
6
9
  import type { ShellProcess } from '@deepseek-ai/dsh-shell';
7
- import type { JobHooks } from '@deepseek-ai/dsh-jobs';
10
+ import type { JobChunk, JobHooks, JobOutcome, JobOutputSource } from '@deepseek-ai/dsh-jobs';
8
11
  /**
9
- * Map a settled background process onto the generic task-outcome vocabulary:
12
+ * Map a settled background process onto the generic job-outcome vocabulary:
10
13
  * `killed` stays `killed` (detail: the signal when one is known), everything
11
14
  * else is `completed` with the exit code as detail. A nonzero command exit is
12
- * reported, not failed, exactly like the foreground rendering.
15
+ * reported, not failed, exactly like the foreground rendering. Sandbox facts
16
+ * join the detail, since a job's terminal reason is the one line every
17
+ * reader — the model's status line, the roster row — shows.
13
18
  * @param proc - the settled process handle.
19
+ * @param escalationModes - escalation targets advertised by this composition.
14
20
  * @returns the outcome for the `ctx.jobs` registration.
15
21
  */
16
- export declare function processOutcome(proc: ShellProcess): {
17
- status: 'completed' | 'killed';
18
- detail: string;
19
- };
22
+ export declare function processOutcome(proc: ShellProcess, escalationModes?: readonly SandboxMode[]): JobOutcome;
23
+ /**
24
+ * The process's non-consuming stream readers as registry pull sources. They
25
+ * bind lazily because the process is spawned inside the starter, after the
26
+ * registry admitted the job; a read before the spawn yields nothing, and the
27
+ * pump keeps the model's consuming cursor untouched. A rejected spawn's
28
+ * stderr reader carries the provider's `subprocess failed before reporting an
29
+ * outcome: …` note.
30
+ * @param proc - the started process's observed streams, once the starter has spawned it.
31
+ * @returns one source per stream, stdout first.
32
+ */
33
+ export declare function processSources(proc: () => Pick<ShellProcess, 'observed'> | undefined): JobOutputSource[];
34
+ /**
35
+ * The ring chunks of one consuming registry read as the shell tools render a
36
+ * process read: stdout chunks in order, then every stderr chunk in one
37
+ * `[stderr]` section, so the output a foreground call hands over when it
38
+ * stops waiting reads exactly like the `job_output` reads that follow it.
39
+ * @param chunks - the chunks since the model cursor, in offset order.
40
+ * @returns the delta text, possibly empty.
41
+ */
42
+ export declare function ringDelta(chunks: readonly JobChunk[]): string;
20
43
  /**
21
44
  * Adapt asynchronous shell preparation after job admission without exposing a partial process.
22
45
  * @param start - starts the process with job-owned cancellation.
23
- * @param renderOutput - consumes output from a published process.
46
+ * @param outcome - projects the settled process into the job outcome.
24
47
  * @returns synchronous job hooks whose completion includes preparation and process settlement.
25
48
  */
26
- export declare function processJob(start: (signal: AbortSignal) => Promise<ShellProcess>, renderOutput: (process: ShellProcess) => string): JobHooks;
49
+ export declare function processJob(start: (signal: AbortSignal) => Promise<ShellProcess>, outcome: (process: ShellProcess) => JobOutcome): JobHooks;
27
50
  //# sourceMappingURL=background.d.ts.map
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Model-facing Consumer of the `ctx.shell` capability seam. Background calls
3
- * register process handles with `ctx.jobs`; their work uses job cancellation
4
- * rather than the tool-call signal after an id is returned.
2
+ * Model-facing Consumer of the `ctx.shell` capability seam. While a job
3
+ * registry is composed, every call registers its process with `ctx.jobs` as
4
+ * it starts: `run_in_background` returns the id at once, and a foreground call
5
+ * waits on its job until the command finishes or the wait times out, at which
6
+ * point it returns the same id. Without a registry the tool is foreground-only
7
+ * and the executor's deadline kills the command.
5
8
  *
6
9
  * TODO(permissions): deployment policy belongs in `tools/pre-execute` and
7
10
  * sandboxing executors; see docs/architecture.md § Where new behavior goes.
@@ -13,8 +16,21 @@ export declare const name = "tool-bash";
13
16
  export declare const inject: string[];
14
17
  /** Configuration for the bash tool. */
15
18
  export interface Config {
16
- /** Expose `run_in_background` (default true); disabled calls are also rejected. */
19
+ /**
20
+ * Expose `run_in_background` while a job registry is composed (default
21
+ * true); disabled calls are also rejected. Without a registry the tool is
22
+ * foreground-only regardless.
23
+ */
17
24
  enableRunInBackground?: boolean;
25
+ /**
26
+ * Keep a foreground command that reaches its timeout running as a
27
+ * background job instead of killing it (default true). Applies only while
28
+ * background execution is available: with `enableRunInBackground` false or
29
+ * no job registry, the executor's deadline kills the command. A foreground
30
+ * command the registry refuses at its start (admission or a missing
31
+ * controller) also runs under the deadline kill.
32
+ */
33
+ promoteOnTimeout?: boolean;
18
34
  }
19
35
  /** Runtime configuration schema for the bash tool plugin. */
20
36
  export declare const Config: z<Config>;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module @deepseek-ai/dsh-tool-bash/render
5
5
  */
6
- import type { ShellProcessRead, ShellRunResult, ShellSandboxInfo } from '@deepseek-ai/dsh-shell';
6
+ import type { ShellRunResult, ShellSandboxInfo } from '@deepseek-ai/dsh-shell';
7
7
  import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
8
8
  /**
9
9
  * Shape one finished run into the text the model sees: stdout, then a marked
@@ -14,20 +14,39 @@ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
14
14
  * @param escalationModes - the escalation targets this composition advertises;
15
15
  * non-empty adds the same-turn escalation hint after a denial marker
16
16
  * (default `[]`: no hint).
17
- * @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
17
+ * @returns the model-facing text: output body (or `(no output)`), then any timeout/stopped/signal/exit markers, each on its own line.
18
18
  */
19
- export declare function renderResult(result: ShellRunResult, escalationModes?: readonly SandboxMode[]): string;
19
+ export declare function renderResult(result: ShellRunResult & {
20
+ stopped?: string;
21
+ }, escalationModes?: readonly SandboxMode[]): string;
20
22
  /**
21
- * Shape one background-process read into the `job_output` delta the model
22
- * sees: the incremental delta, plus the lossy-read notice (with full-stream
23
- * spill paths) when in-memory truncation dropped unread bytes. Empty-delta
24
- * rendering (`(no new output)`) is the generic job controller's job.
25
- * @param read - one incremental read from the process handle.
23
+ * Shape a foreground call that stopped waiting into the text the model sees:
24
+ * the output captured so far (one consuming registry read taken at that
25
+ * point, so `job_output` continues exactly after it), then the still-running
26
+ * marker and the job hand-off guidance.
27
+ * @param promoted - the promoted result value: the job id, the wait that
28
+ * expired, and the output so far.
29
+ * @returns the model-facing text for a promoted call.
30
+ */
31
+ export declare function renderPromoted(promoted: {
32
+ jobId: string;
33
+ timeoutMs: number;
34
+ output: string;
35
+ }): string;
36
+ /**
37
+ * Shape the one consuming registry read a foreground call embeds in its
38
+ * result when it stops waiting: the output produced so far, plus the
39
+ * dropped-output notice (naming the job's spill files) when the model cursor
40
+ * fell behind the ring, and the sandbox notices. Later `job_output` reads
41
+ * render the same ring through the job tools.
42
+ * @param delta - the read's chunks as rendered text.
43
+ * @param lossy - whether bytes before the delta were evicted unread.
44
+ * @param spillPaths - the complete-stream files the job currently advertises.
26
45
  * @param sandbox - settled sandbox facts, when this was a confined process.
27
46
  * @param escalationModes - escalation targets advertised by this composition.
28
47
  * @returns the delta text with any loss or sandbox notice appended.
29
48
  */
30
- export declare function renderProcessRead(read: ShellProcessRead, sandbox?: ShellSandboxInfo, escalationModes?: readonly SandboxMode[]): string;
49
+ export declare function renderJobRead(delta: string, lossy: boolean, spillPaths: readonly string[], sandbox?: ShellSandboxInfo, escalationModes?: readonly SandboxMode[]): string;
31
50
  /**
32
51
  * The exit-status parse is the shared marker-contract half of the shell-tool
33
52
  * rendering story, owned by `@deepseek-ai/dsh-shell` so `dsh-tool-pwsh` reuses
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-tool-bash",
3
3
  "description": "Model-facing bash tool with optional generic background-job and sandbox-escalation support",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,41 +27,41 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
31
- "@deepseek-ai/dsh-shell-env": "^0.1.6-alpha.2",
32
- "@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
33
- "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
34
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2",
35
- "@deepseek-ai/dsh-jobs": "^0.1.6-alpha.2",
36
- "@deepseek-ai/dsh-user-approval": "^0.1.6-alpha.2",
37
- "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
38
- "@deepseek-ai/cordis": "^4.0.2",
39
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
40
- "@deepseek-ai/dsh-agent": "^0.1.6-alpha.2"
30
+ "@deepseek-ai/dsh-shell": "^0.1.7-alpha.1",
31
+ "@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
32
+ "@deepseek-ai/dsh-shell-env": "^0.1.7-alpha.1",
33
+ "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
34
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.7-alpha.1",
35
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
36
+ "@deepseek-ai/dsh-sandbox": "^0.1.7-alpha.1",
37
+ "@deepseek-ai/dsh-jobs": "^0.1.7-alpha.1",
38
+ "@deepseek-ai/dsh-user-approval": "^0.1.7-alpha.1",
39
+ "@deepseek-ai/cordis": "^4.0.3",
40
+ "@deepseek-ai/dsh-tools": "^0.1.7-alpha.1"
41
41
  },
42
42
  "dependencies": {
43
- "@deepseek-ai/schemastery": "^3.18.2"
43
+ "@deepseek-ai/schemastery": "^3.18.3"
44
44
  },
45
45
  "devDependencies": {
46
- "@deepseek-ai/dsh-agent": "^0.1.6-alpha.2",
47
- "@deepseek-ai/dsh-agent-loop": "^0.1.6-alpha.2",
48
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.6-alpha.2",
49
- "@deepseek-ai/dsh-shell-env": "^0.1.6-alpha.2",
50
- "@deepseek-ai/dsh-bash-local": "^0.1.6-alpha.2",
51
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
52
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
53
- "@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
54
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2",
55
- "@deepseek-ai/dsh-subprocess-local": "^0.1.6-alpha.2",
56
- "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
57
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
58
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.6-alpha.2",
59
- "@deepseek-ai/dsh-jobs": "^0.1.6-alpha.2",
60
- "@deepseek-ai/dsh-jobs-local": "^0.1.6-alpha.2",
61
- "@deepseek-ai/dsh-tool-jobs": "^0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-user-approval": "^0.1.6-alpha.2",
64
- "@deepseek-ai/cordis": "^4.0.2",
65
- "@deepseek-ai/dsh-session-projection": "^0.1.6-alpha.2"
46
+ "@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
47
+ "@deepseek-ai/dsh-agent-loop": "^0.1.7-alpha.1",
48
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.7-alpha.1",
49
+ "@deepseek-ai/dsh-shell": "^0.1.7-alpha.1",
50
+ "@deepseek-ai/dsh-shell-env": "^0.1.7-alpha.1",
51
+ "@deepseek-ai/dsh-bash-local": "^0.1.7-alpha.1",
52
+ "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
53
+ "@deepseek-ai/dsh-subprocess-local": "^0.1.7-alpha.1",
54
+ "@deepseek-ai/dsh-sandbox": "^0.1.7-alpha.1",
55
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.7-alpha.1",
56
+ "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
57
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.7-alpha.1",
58
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
59
+ "@deepseek-ai/dsh-jobs-local": "^0.1.7-alpha.1",
60
+ "@deepseek-ai/dsh-tool-jobs": "^0.1.7-alpha.1",
61
+ "@deepseek-ai/dsh-jobs": "^0.1.7-alpha.1",
62
+ "@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
63
+ "@deepseek-ai/dsh-user-approval": "^0.1.7-alpha.1",
64
+ "@deepseek-ai/cordis": "^4.0.3",
65
+ "@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1"
66
66
  }
67
67
  }