@deepseek-ai/dsh-bash-local 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/bash-local/README.md
5
- README.md: 2cd69993b454a462bcbfa43175ff7dc7dba72e01
6
- README.zh.md: 81436511992d93a2bb81c4f3e242dd2046ec11e8
5
+ README.md: 578e5284e22d6ec6dda1590848734ac63d2399fc
6
+ README.zh.md: 4bac3b6cceced30935108ffd14665df51a0c23ce
package/README.md CHANGED
@@ -52,21 +52,22 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
52
52
 
53
53
  ### Running commands
54
54
 
55
- Run a command with `run` and read its output from the result. A nonzero exit, a timeout, or a cancellation resolves with a descriptive result — only infrastructure failures reject. Per-call `timeoutMs` overrides are capped by the configuration, while `workdir` falls back to the configured default when unset; a trusted foreground caller can also raise the stdout capture budget for one call, while stderr and background runs keep `maxOutputBytes`. The environment is model-friendly by default: `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` keep pagers and ANSI colors from garbling output, and an explicit caller-provided entry still wins.
55
+ Run a command by awaiting the execution's `result()` projection. A nonzero exit, a timeout, or a cancellation resolves with a descriptive result — only infrastructure failures reject. Per-call `timeoutMs` overrides are capped by the configuration, while `workdir` falls back to the configured default when unset; a trusted caller can also raise the per-call stdout capture budget, while stderr keeps `maxOutputBytes`. The environment is model-friendly by default: `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` keep pagers and ANSI colors from garbling output, and an explicit caller-provided entry still wins.
56
56
 
57
57
  ```text
58
- const result = await ctx.shell.run(ctx.shell.resolve({ command: 'ls -la' }))
58
+ const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'ls -la' }))
59
+ const result = await execution.result()
59
60
  if (result.timedOut) console.log('timed out after', result.timeoutMs)
60
61
  ```
61
62
 
62
63
  ### Background processes
63
64
 
64
- Await `start` to run a command in the background; it resolves with the prepared process handle and no execution timeout applies. Cancellation or preparation failure rejects before a handle is published. `readOutput()` merges the stream deltas into one consuming read, marking stderr under a `[stderr]` section; `kill()` terminates the provider-managed range; `done` settles when the direct command closes and never rejects. Job ids, ownership, polling, and notices belong to the generic `ctx.jobs` runtime, which the tool layer registers the handle with.
65
+ Resolve with `onExpiry: 'none'` and await `execute` to run a command in the background; no deadline is armed. Cancellation or preparation failure rejects before a handle is published. `readOutput()` merges the stream deltas into one consuming read, marking stderr under a `[stderr]` section; `kill()` terminates the provider-managed range; `done` settles when the direct command closes and never rejects. Job ids, ownership, polling, and notices belong to the generic `ctx.jobs` runtime, which the tool layer registers the handle with.
65
66
 
66
67
  <a id="adjusting-budgets-at-runtime"></a>
67
68
  ### Adjusting budgets at runtime
68
69
 
69
- When a settings provider is composed, this executor registers the capability's shared `shell` settings namespace with the composition entry as its base, so a user section in `settings.yaml` layers over it and the next command runs with the new budgets. Values the schema cannot judge — positive and finite numbers, and the `graceMs` timer bound — are refused at the write, leaving the running executor on its last good section; without a provider, the composition entry is what runs.
70
+ Execution budgets are volatile Config fields sampled when resolving each command. The Plugins page edits the active executor’s profile entry. Complete Config validation rejects invalid numbers and timer limits before a form write reaches disk.
70
71
 
71
72
  -----
72
73
 
@@ -93,7 +94,7 @@ The executor is a Service Provider for the `ctx.shell` seam built on the subproc
93
94
 
94
95
  ### Main flow
95
96
 
96
- A call runs through three steps: `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config (capping per-call overrides); `run` fuses the config-clamped timeout with the caller's abort signal into one deadline and spawns `['bash', '-c', command]` through `ctx.subprocess` with explicit byte caps and the `graceMs`; the settled subprocess outcome is classified — only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, a self-signaled command reports neither — and projected into a `ShellRunResult` with collected output.
97
+ A call runs through three steps: `resolve()` fills `workdir`/`timeoutMs`/`onExpiry`/`stdoutMaxBytes` from config and the request (capping per-call overrides); `execute` wires the deadline per expiry policy — `'kill'` fuses the clamped timeout with the caller's abort signal, `'none'` arms nothing — and spawns `['bash', '-c', command]` through `ctx.subprocess` with explicit byte caps and the `graceMs`; the settled outcome is classified first-cause — only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, a self-signaled command reports neither — and `result()` projects it into a `ShellRunResult` with collected output.
97
98
 
98
99
  The foreground deadline starts before argv preparation and retains the same signal and remaining budget through execution. Preparation timeout returns empty output, `timedOut: true`, and null `exitCode` and `signal`; caller cancellation before process publication still rejects. Late preparation success or failure cannot trigger a spawn.
99
100
 
@@ -139,7 +140,7 @@ These limits define when this executor is a poor fit. They are current package c
139
140
  - **Unconfined by itself** — commands run with the harness process's authority; deployments needing confinement compose `dsh-bash-sandbox`, while per-call allow/deny/ask policy belongs on the tools' `pre-execute` waterfall.
140
141
  - **No persistent shell or PTY** — every call starts a fresh non-login `bash -c`; cwd-only persistence and interactive terminal sessions remain deferred until a real workflow requires them.
141
142
  - **POSIX-only** — the `bash` binary is hardcoded and the underlying service's group semantics are POSIX; Windows is unsupported.
142
- - **A background provider-failure note is single-delivery** — `SubprocessHandle.done` can reject before or after target execution begins, so the executor injects the stage-neutral `subprocess failed before reporting an outcome: …` into exactly one `readOutput()` delta; a reader that discards that delta cannot recover it.
143
+ - **A background provider-failure note is the whole stderr stream** — `SubprocessHandle.done` can reject before or after target execution begins, and the subprocess service buffers no output for a target that never reported, so the executor serves the stage-neutral `subprocess failed before reporting an outcome: …` as the observed stderr stream (offset readers re-read it at their own offsets) and folds it into exactly one `readOutput()` delta; a consuming reader that discards that delta recovers it only through `observed.stderr`.
143
144
 
144
145
  <a id="dev-note"></a>
145
146
  ### Dev Note
package/README.zh.md CHANGED
@@ -52,21 +52,22 @@ kind: "package-reference"
52
52
 
53
53
  ### 运行命令
54
54
 
55
- 用 `run` 运行命令并从结果读取输出。非零退出、超时或取消都会 resolve 为描述性结果——只有基础设施失败才 reject。每次调用的 `timeoutMs` 覆盖值受配置上限约束,`workdir` 未设置时则回退到配置的默认值;受信任的前台调用方还可以为单次调用提高 stdout 捕获预算,而 stderr 与后台运行仍使用 `maxOutputBytes`。环境默认面向模型:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 可防止分页器与 ANSI 颜色破坏输出,调用方显式提供的条目仍然优先。
55
+ 通过等待 execution 的 `result()` 投影来运行命令。非零退出、超时或取消都会 resolve 为描述性结果——只有基础设施失败才 reject。每次调用的 `timeoutMs` 覆盖值受配置上限约束,`workdir` 未设置时则回退到配置的默认值;受信任的调用方还可以为单次调用提高 stdout 捕获预算,而 stderr 仍使用 `maxOutputBytes`。环境默认面向模型:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 可防止分页器与 ANSI 颜色破坏输出,调用方显式提供的条目仍然优先。
56
56
 
57
57
  ```text
58
- const result = await ctx.shell.run(ctx.shell.resolve({ command: 'ls -la' }))
58
+ const execution = await ctx.shell.execute(ctx.shell.resolve({ command: 'ls -la' }))
59
+ const result = await execution.result()
59
60
  if (result.timedOut) console.log('timed out after', result.timeoutMs)
60
61
  ```
61
62
 
62
63
  ### 后台进程
63
64
 
64
- 等待 `start` 即可在后台运行命令;它完成准备后返回进程句柄,且不应用执行超时。取消或准备失败会在发布句柄前拒绝调用。`readOutput()` 把流增量合并为一次消费式读取,并在 `[stderr]` 分段下标记 stderr;`kill()` 终止提供方管理的 range;`done` 在直接命令关闭时结算且绝不 reject。job id、所有权、轮询与通知属于通用 `ctx.jobs` 运行时,工具层会把句柄注册进去。
65
+ 以 `onExpiry: 'none'` 解析并等待 `execute` 返回句柄即可在后台运行命令;不布置任何 deadline。取消或准备失败会在发布句柄前拒绝调用。`readOutput()` 把流增量合并为一次消费式读取,并在 `[stderr]` 分段下标记 stderr;`kill()` 终止提供方管理的 range;`done` 在直接命令关闭时结算且绝不 reject。job id、所有权、轮询与通知属于通用 `ctx.jobs` 运行时,工具层会把句柄注册进去。
65
66
 
66
67
  <a id="adjusting-budgets-at-runtime"></a>
67
68
  ### 运行时调整预算
68
69
 
69
- 当组合了设置提供方时,本执行器以组合条目为 base 注册该能力共享的 `shell` 设置命名空间,因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。schema 无法判定的值——正有限数字与 `graceMs` 的定时器上界——会在写入时被拒绝,运行中的执行器保持它最后一份可用的段;没有提供方时,运行的就是组合条目。
70
+ 执行预算是解析每条命令时读取的 volatile Config 字段。插件页面编辑当前执行器的 profile 条目。完整 Config 验证在表单写入磁盘前拒绝无效数字和定时器上限。
70
71
 
71
72
  -----
72
73
 
@@ -93,7 +94,7 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
93
94
 
94
95
  ### 主要流程
95
96
 
96
- 一次调用分三步:`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`(并限制每次调用的覆盖值);`run` 把按配置钳位的超时与调用方的中止信号融合为一个 deadline,再以显式字节上限与 `graceMs` 通过 `ctx.subprocess` spawn `['bash', '-c', command]`;结算的 subprocess 结果被分类——只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告——并投影为带收集输出的 `ShellRunResult`。
97
+ 一次调用分三步:`resolve()` 从配置与请求填充 `workdir`/`timeoutMs`/`onExpiry`/`stdoutMaxBytes`(并限制每次调用的覆盖值);`execute` 按到期策略布置 deadline——`'kill'` 把钳位后的超时与调用方的中止信号融合为一个 deadline,`'none'` 不布置任何 deadline——再以显式字节上限与 `graceMs` 通过 `ctx.subprocess` spawn `['bash', '-c', command]`;结算的 subprocess 结果被分类——只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告——并投影为带收集输出的 `ShellRunResult`。
97
98
 
98
99
  前台 deadline 从 argv 准备开始,并在准备与执行之间保持同一信号和剩余预算。准备阶段超时返回空输出、`timedOut: true`,且 `exitCode` 和 `signal` 均为 `null`;调用方在发布进程前取消仍会拒绝调用。准备晚到的成功或失败不会触发 spawn。
99
100
 
@@ -139,7 +140,7 @@ if (result.timedOut) console.log('timed out after', result.timeoutMs)
139
140
  - **自身不提供隔离**——命令以 harness 进程的权限运行;需要隔离的部署组合 `dsh-bash-sandbox`,每次调用的 allow/deny/ask 策略则属于工具的 `pre-execute` waterfall(瀑布式事件)。
140
141
  - **没有持久 shell 或 PTY**——每次调用都启动全新的非登录 `bash -c`;仅持久化 cwd 与交互式终端会话均继续延期,直到真实工作流需要它们。
141
142
  - **仅支持 POSIX**——`bash` 二进制已硬编码,底层服务的进程组语义也是 POSIX 的;不支持 Windows。
142
- - **后台提供方失败提示只交付一次**——`SubprocessHandle.done` 可能在目标命令开始执行前或后被拒绝,因此执行器把不声明失败阶段的 `subprocess failed before reporting an outcome: …` 注入恰好一个 `readOutput()` 增量;丢弃了该增量的读取方无法再恢复它。
143
+ - **后台提供方失败提示就是整条 stderr 流**——`SubprocessHandle.done` 可能在目标命令开始执行前或后被拒绝,而 subprocess 服务不会为从未上报结果的目标命令 缓冲任何输出,因此执行器把不声明失败阶段的 `subprocess failed before reporting an outcome: …` 作为观测到的 stderr 流提供(偏移读取方按各自偏移重读),并只折入恰好一个 `readOutput()` 增量;丢弃了该增量的消耗式读取方只能经 `observed.stderr` 恢复它。
143
144
 
144
145
  <a id="dev-note"></a>
145
146
  ### 开发备注
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { SHELL_SETTINGS_NAMESPACE, ShellExecutor } from "@deepseek-ai/dsh-shell";
2
+ import { ShellExecutor } from "@deepseek-ai/dsh-shell";
3
3
  import { MAX_TIMER_DELAY_MS, clampTimeout, deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
4
4
  //#region lib/types/index.js
5
5
  /**
@@ -12,64 +12,6 @@ import { MAX_TIMER_DELAY_MS, clampTimeout, deadline, timeoutOf } from "@deepseek
12
12
  * `tools/pre-execute` or a sandboxing executor.
13
13
  * @module @deepseek-ai/dsh-bash-local
14
14
  */
15
- var __addDisposableResource = function(env, value, async) {
16
- if (value !== null && value !== void 0) {
17
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
18
- var dispose, inner;
19
- if (async) {
20
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
21
- dispose = value[Symbol.asyncDispose];
22
- }
23
- if (dispose === void 0) {
24
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
25
- dispose = value[Symbol.dispose];
26
- if (async) inner = dispose;
27
- }
28
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
29
- if (inner) dispose = function() {
30
- try {
31
- inner.call(this);
32
- } catch (e) {
33
- return Promise.reject(e);
34
- }
35
- };
36
- env.stack.push({
37
- value,
38
- dispose,
39
- async
40
- });
41
- } else if (async) env.stack.push({ async: true });
42
- return value;
43
- };
44
- var __disposeResources = (function(SuppressedError) {
45
- return function(env) {
46
- function fail(e) {
47
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
48
- env.hasError = true;
49
- }
50
- var r, s = 0;
51
- function next() {
52
- while (r = env.stack.pop()) try {
53
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
54
- if (r.dispose) {
55
- var result = r.dispose.call(r.value);
56
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
57
- fail(e);
58
- return next();
59
- });
60
- } else s |= 1;
61
- } catch (e) {
62
- fail(e);
63
- }
64
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
65
- if (env.hasError) throw env.error;
66
- }
67
- return next();
68
- };
69
- })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
70
- var e = new Error(message);
71
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
72
- });
73
15
  /**
74
16
  * Model-friendly environment overrides: disable colors, pagers, and
75
17
  * interactive terminal features that would garble tool output (the same set
@@ -102,19 +44,17 @@ function assertPositiveFinite(name, value) {
102
44
  /**
103
45
  * Reject a resolved section this executor could not run with. The schema
104
46
  * expresses neither "positive and finite" nor the timer bound `graceMs` has to
105
- * fit, so a stored value is refused where it is written instead of failing at
106
- * the next command.
107
- * @param config - the resolved section, schema-valid by construction.
47
+ * fit, so a stored value that cannot be used fails at the next command.
48
+ * @param config - the live configuration, schema-valid by construction.
108
49
  * @throws Error naming the field that cannot be used.
109
50
  */
110
51
  function assertServiceableBashConfig(config) {
111
- const resolved = config;
112
- assertPositiveFinite("timeoutMs", resolved.timeoutMs);
113
- assertPositiveFinite("maxTimeoutMs", resolved.maxTimeoutMs);
114
- assertPositiveFinite("maxOutputBytes", resolved.maxOutputBytes);
115
- assertPositiveFinite("maxSpillBytes", resolved.maxSpillBytes);
116
- assertPositiveFinite("graceMs", resolved.graceMs);
117
- if (resolved.graceMs > MAX_TIMER_DELAY_MS) throw new Error(`bash-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
52
+ assertPositiveFinite("timeoutMs", config.timeoutMs.get());
53
+ assertPositiveFinite("maxTimeoutMs", config.maxTimeoutMs.get());
54
+ assertPositiveFinite("maxOutputBytes", config.maxOutputBytes.get());
55
+ assertPositiveFinite("maxSpillBytes", config.maxSpillBytes.get());
56
+ assertPositiveFinite("graceMs", config.graceMs.get());
57
+ if (config.graceMs.get() > MAX_TIMER_DELAY_MS) throw new Error(`bash-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
118
58
  }
119
59
  /**
120
60
  * Local bash executor over `ctx.subprocess`. Bounded output, spill files,
@@ -124,51 +64,37 @@ function assertServiceableBashConfig(config) {
124
64
  * composition teardown) even across an executor reload.
125
65
  */
126
66
  var LocalBashExecutor = class LocalBashExecutor extends ShellExecutor {
67
+ config;
127
68
  static inject = ["subprocess"];
128
69
  static Config = z.object({
129
- cwd: z.string(),
130
- timeoutMs: z.number().default(12e4),
131
- maxTimeoutMs: z.number().default(6e5),
132
- maxOutputBytes: z.number().default(64e3),
133
- maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES),
134
- graceMs: z.number().default(DEFAULT_GRACE_MS)
70
+ cwd: z.string().volatile(),
71
+ timeoutMs: z.number().default(12e4).volatile(),
72
+ maxTimeoutMs: z.number().default(6e5).volatile(),
73
+ maxOutputBytes: z.number().default(64e3).volatile(),
74
+ maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES).volatile(),
75
+ graceMs: z.number().default(DEFAULT_GRACE_MS).volatile()
135
76
  });
136
- /** The currently authoritative config: the settings section, or the composition entry. */
137
- source;
138
- /** Validated config (schemastery applied the defaults before construction). */
139
- get config() {
140
- return this.source();
141
- }
142
77
  constructor(ctx, config) {
143
78
  super(ctx);
144
- const entry = config;
145
- assertServiceableBashConfig(entry);
146
- this.source = () => entry;
147
- ctx.inject(["settings"], (settingsCtx) => {
148
- settingsCtx.settings.installSection(ctx, SHELL_SETTINGS_NAMESPACE, LocalBashExecutor.Config, entry, {
149
- validate: assertServiceableBashConfig,
150
- setSource: (current) => {
151
- this.source = current;
152
- },
153
- onChange: () => {}
154
- });
155
- });
79
+ this.config = config;
156
80
  }
157
81
  /**
158
82
  * Resolve a request into a fully-specified spec: fill `workdir` from
159
83
  * `config.cwd` (else `process.cwd()`), and `timeoutMs` from
160
84
  * `config.timeoutMs`, capped at `config.maxTimeoutMs`. The tool layer calls
161
- * this before {@link run}/{@link start}, so those methods receive explicit
162
- * values and never re-default.
85
+ * this before {@link execute}, so it receives explicit values and never
86
+ * re-defaults.
163
87
  */
164
88
  resolve(request) {
165
- const timeoutMs = clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, "bash-local: request.timeoutMs");
166
- const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes;
89
+ assertServiceableBashConfig(this.config);
90
+ const timeoutMs = clampTimeout(request.timeoutMs, this.config.timeoutMs.get(), this.config.maxTimeoutMs.get(), "bash-local: request.timeoutMs");
91
+ const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes.get();
167
92
  assertPositiveFinite("request.stdoutMaxBytes", stdoutMaxBytes);
168
93
  return {
169
94
  command: request.command,
170
- workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
95
+ workdir: request.workdir ?? this.config.cwd.get() ?? process.cwd(),
171
96
  timeoutMs,
97
+ onExpiry: request.onExpiry ?? "kill",
172
98
  stdoutMaxBytes,
173
99
  ...request.signal ? { signal: request.signal } : {},
174
100
  ...request.stdin !== void 0 ? { stdin: request.stdin } : {},
@@ -181,7 +107,7 @@ var LocalBashExecutor = class LocalBashExecutor extends ShellExecutor {
181
107
  spawnSpec(spec, argv, stdoutMaxBytes, signal) {
182
108
  const collect = (maxBytes) => ({
183
109
  maxBytes,
184
- spill: { maxBytes: this.config.maxSpillBytes }
110
+ spill: { maxBytes: this.config.maxSpillBytes.get() }
185
111
  });
186
112
  return {
187
113
  argv,
@@ -189,9 +115,9 @@ var LocalBashExecutor = class LocalBashExecutor extends ShellExecutor {
189
115
  stdio: {
190
116
  stdin: spec.stdin !== void 0 ? { data: spec.stdin } : "ignore",
191
117
  stdout: collect(stdoutMaxBytes),
192
- stderr: collect(this.config.maxOutputBytes)
118
+ stderr: collect(this.config.maxOutputBytes.get())
193
119
  },
194
- graceMs: this.config.graceMs,
120
+ graceMs: this.config.graceMs.get(),
195
121
  signal,
196
122
  env: {
197
123
  ...ENV_OVERRIDES,
@@ -211,134 +137,144 @@ var LocalBashExecutor = class LocalBashExecutor extends ShellExecutor {
211
137
  stderr
212
138
  };
213
139
  }
214
- async run(spec) {
215
- return (await this.runArgv(spec, [
140
+ async execute(spec) {
141
+ return this.executeArgv(spec, [
216
142
  "bash",
217
143
  "-c",
218
144
  spec.command
219
- ])).result;
145
+ ]);
220
146
  }
221
147
  /**
222
- * Run an explicit argv with the foreground lifecycle, environment, output,
223
- * timeout, and cancellation semantics of this executor. Subclasses use this
148
+ * Execute an explicit argv with the lifecycle, environment, output,
149
+ * deadline, and cancellation semantics of this executor. Subclasses use this
224
150
  * after replacing the public command's shell argv at an execution boundary.
225
151
  * @param spec - resolved execution settings and caller-owned command metadata.
226
- * @param argvOrPrepare - exact argv, or preparation cancelled by the same deadline as execution.
227
- * @returns the foreground result and whether argv reached the subprocess provider.
152
+ * @param argvOrPrepare - exact argv, or preparation using the execution cancellation signal.
153
+ * @param onStarted - installs provider facts synchronously before the handle can settle.
154
+ * @returns the live execution handle; spawn rejection settles the handle as
155
+ * killed while `result()` carries the same failure as its rejection.
228
156
  */
229
- async runArgv(spec, argvOrPrepare) {
230
- const env_1 = {
231
- stack: [],
232
- error: void 0,
233
- hasError: false
234
- };
235
- try {
236
- const d = __addDisposableResource(env_1, deadline(spec.signal, spec.timeoutMs, "BASH_TIMEOUT"), false);
237
- let argv;
238
- if (typeof argvOrPrepare === "function") {
239
- const cancelled = Promise.withResolvers();
240
- const abort = () => {
241
- cancelled.reject(d.signal.reason);
242
- };
243
- d.signal.addEventListener("abort", abort, { once: true });
244
- try {
245
- argv = await Promise.race([Promise.resolve().then(() => {
246
- d.signal.throwIfAborted();
247
- return argvOrPrepare(d.signal);
248
- }), cancelled.promise]);
249
- d.signal.throwIfAborted();
250
- } catch (error) {
251
- if (timeoutOf(d.signal, "BASH_TIMEOUT") === void 0) throw error;
252
- return {
253
- spawnRequested: false,
254
- result: {
255
- exitCode: null,
256
- signal: null,
257
- timedOut: true,
258
- aborted: false,
259
- timeoutMs: spec.timeoutMs,
260
- stdout: {
261
- text: "",
262
- truncated: false
263
- },
264
- stderr: {
265
- text: "",
266
- truncated: false
267
- }
268
- }
269
- };
270
- } finally {
271
- d.signal.removeEventListener("abort", abort);
272
- }
273
- } else argv = argvOrPrepare;
274
- const handle = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, spec.stdoutMaxBytes, d.signal));
275
- const outcome = await handle.done;
276
- const collected = LocalBashExecutor.collected(handle);
277
- const timedOut = timeoutOf(d.signal, "BASH_TIMEOUT") !== void 0;
278
- const aborted = d.signal.aborted && !timedOut;
279
- return {
280
- spawnRequested: true,
281
- result: {
282
- ...outcome,
157
+ async executeArgv(spec, argvOrPrepare, onStarted) {
158
+ let spawnSignal;
159
+ let classify;
160
+ let disarm = () => {};
161
+ if (spec.onExpiry === "kill") {
162
+ const d = deadline(spec.signal, spec.timeoutMs, "BASH_TIMEOUT");
163
+ spawnSignal = d.signal;
164
+ classify = () => {
165
+ const timedOut = timeoutOf(d.signal, "BASH_TIMEOUT") !== void 0;
166
+ return {
283
167
  timedOut,
284
- aborted,
285
- timeoutMs: spec.timeoutMs,
286
- stdout: finalOutput(collected.stdout),
287
- stderr: finalOutput(collected.stderr)
288
- }
168
+ aborted: d.signal.aborted && !timedOut
169
+ };
170
+ };
171
+ disarm = () => {
172
+ d[Symbol.dispose]();
289
173
  };
290
- } catch (e_1) {
291
- env_1.error = e_1;
292
- env_1.hasError = true;
293
- } finally {
294
- __disposeResources(env_1);
174
+ } else {
175
+ spawnSignal = spec.signal;
176
+ classify = () => ({
177
+ timedOut: false,
178
+ aborted: spec.signal?.aborted === true
179
+ });
295
180
  }
296
- }
297
- async start(spec) {
298
- return Promise.resolve(this.startArgv(spec, [
299
- "bash",
300
- "-c",
301
- spec.command
302
- ]));
303
- }
304
- /**
305
- * Start an explicit argv with the background lifecycle, environment, output,
306
- * cancellation, and managed-range ownership semantics of this executor.
307
- * Subclasses use this after replacing the public command's shell argv at an
308
- * execution boundary.
309
- * @param spec - resolved execution settings and caller-owned command metadata.
310
- * @param argv - exact executable and arguments to hand to `ctx.subprocess`.
311
- * @returns the live background handle; provider rejection settles it as killed.
312
- */
313
- startArgv(spec, argv) {
314
- spec.signal?.throwIfAborted();
315
- const running = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, this.config.maxOutputBytes, spec.signal));
316
- const collected = LocalBashExecutor.collected(running);
317
- let providerFailureNote;
181
+ let argv = [];
182
+ let preparationTimedOut = false;
183
+ if (typeof argvOrPrepare === "function") {
184
+ const signal = spawnSignal ?? new AbortController().signal;
185
+ const cancelled = Promise.withResolvers();
186
+ const abort = () => {
187
+ cancelled.reject(signal.reason);
188
+ };
189
+ signal.addEventListener("abort", abort, { once: true });
190
+ try {
191
+ argv = await Promise.race([Promise.resolve().then(() => {
192
+ signal.throwIfAborted();
193
+ return argvOrPrepare(signal);
194
+ }), cancelled.promise]);
195
+ signal.throwIfAborted();
196
+ } catch (error) {
197
+ if (!classify().timedOut) {
198
+ disarm();
199
+ throw error;
200
+ }
201
+ preparationTimedOut = true;
202
+ } finally {
203
+ signal.removeEventListener("abort", abort);
204
+ }
205
+ } else argv = argvOrPrepare;
206
+ let running;
207
+ let syncSpawnError;
208
+ try {
209
+ if (!preparationTimedOut) running = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, spec.stdoutMaxBytes, spawnSignal));
210
+ } catch (error) {
211
+ syncSpawnError = { error };
212
+ }
213
+ const emptyReader = { readFrom: () => ({
214
+ text: "",
215
+ lossy: false,
216
+ nextOffset: 0
217
+ }) };
218
+ const collected = running !== void 0 ? LocalBashExecutor.collected(running) : {
219
+ stdout: emptyReader,
220
+ stderr: emptyReader
221
+ };
222
+ const spawnThrow = () => syncSpawnError.error;
223
+ const spawned = preparationTimedOut ? Promise.resolve({
224
+ exitCode: null,
225
+ signal: null
226
+ }) : running !== void 0 ? running.done : Promise.reject(spawnThrow());
227
+ let providerFailure;
228
+ let providerFailureReported = false;
318
229
  const consumeProviderFailure = () => {
319
- const note = providerFailureNote ?? "";
320
- providerFailureNote = void 0;
321
- return note;
230
+ if (providerFailure === void 0 || providerFailureReported) return "";
231
+ providerFailureReported = true;
232
+ return providerFailure.note;
322
233
  };
234
+ const observedStderr = { readFrom: (fromByte) => {
235
+ if (providerFailure === void 0) return collected.stderr.readFrom(fromByte);
236
+ const note = Buffer.from(providerFailure.note, "utf8");
237
+ return {
238
+ text: note.subarray(Math.min(fromByte, note.length)).toString("utf8"),
239
+ nextOffset: note.length,
240
+ lossy: false
241
+ };
242
+ } };
323
243
  let stdoutOffset = 0;
324
244
  let stderrOffset = 0;
245
+ let resultPromise;
325
246
  const proc = {
326
247
  status: "running",
327
248
  exitCode: null,
328
249
  signal: null,
329
- done: running.done.then((outcome) => {
330
- if (proc.status === "running") proc.status = spec.signal?.aborted === true || outcome.signal !== null ? "killed" : "completed";
250
+ observed: {
251
+ stdout: collected.stdout,
252
+ stderr: observedStderr
253
+ },
254
+ done: spawned.then((outcome) => {
255
+ if (proc.status === "running") proc.status = spawnSignal?.aborted === true || outcome.signal !== null ? "killed" : "completed";
331
256
  proc.exitCode = outcome.exitCode;
332
257
  proc.signal = outcome.signal;
333
258
  this.onProcessDone(proc, collected.stderr.readFrom(0).text, false);
259
+ disarm();
334
260
  }, (error) => {
261
+ if (running !== void 0 && (proc.status === "killed" || spawnSignal?.aborted === true)) {
262
+ proc.status = "killed";
263
+ this.onProcessDone(proc, collected.stderr.readFrom(0).text, false);
264
+ disarm();
265
+ return;
266
+ }
335
267
  proc.status = "killed";
336
268
  let detail = "unprintable provider failure";
337
269
  try {
338
270
  detail = String(error);
339
271
  } catch {}
340
- providerFailureNote = `subprocess failed before reporting an outcome: ${detail}`;
341
- this.onProcessDone(proc, providerFailureNote, true, error);
272
+ providerFailure = {
273
+ error,
274
+ note: `subprocess failed before reporting an outcome: ${detail}`
275
+ };
276
+ this.onProcessDone(proc, providerFailure.note, true, error);
277
+ disarm();
342
278
  }),
343
279
  readOutput: () => {
344
280
  const out = collected.stdout.readFrom(stdoutOffset);
@@ -359,10 +295,25 @@ var LocalBashExecutor = class LocalBashExecutor extends ShellExecutor {
359
295
  kill: () => {
360
296
  if (proc.status !== "running") return false;
361
297
  proc.status = "killed";
362
- running.terminate();
298
+ running?.terminate();
363
299
  return true;
300
+ },
301
+ result: () => {
302
+ resultPromise ??= proc.done.then(() => {
303
+ if (providerFailure !== void 0) throw providerFailure.error;
304
+ return {
305
+ exitCode: proc.exitCode,
306
+ signal: proc.signal,
307
+ ...classify(),
308
+ timeoutMs: spec.timeoutMs,
309
+ stdout: finalOutput(collected.stdout),
310
+ stderr: finalOutput(collected.stderr)
311
+ };
312
+ });
313
+ return resultPromise;
364
314
  }
365
315
  };
316
+ if (!preparationTimedOut) onStarted?.(proc);
366
317
  return proc;
367
318
  }
368
319
  /**
@@ -8,10 +8,11 @@
8
8
  * `tools/pre-execute` or a sandboxing executor.
9
9
  * @module @deepseek-ai/dsh-bash-local
10
10
  */
11
+ import type { Volatile } from '@deepseek-ai/cordis';
11
12
  import { Context } from '@deepseek-ai/cordis';
12
13
  import z from '@deepseek-ai/schemastery';
13
14
  import { ShellExecutor } from '@deepseek-ai/dsh-shell';
14
- import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell';
15
+ import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellProcess } from '@deepseek-ai/dsh-shell';
15
16
  /**
16
17
  * Model-friendly environment overrides: disable colors, pagers, and
17
18
  * interactive terminal features that would garble tool output (the same set
@@ -25,29 +26,26 @@ export declare const ENV_OVERRIDES: {
25
26
  readonly PAGER: "cat";
26
27
  readonly GIT_PAGER: "cat";
27
28
  };
28
- /** Plugin config (all optional — `static Config` supplies the defaults). */
29
+ /** Validated plugin configuration with live command budgets. */
29
30
  export interface Config {
30
31
  /** Default working directory for commands (default: process.cwd()). */
31
- cwd?: string;
32
+ cwd: Volatile<string | undefined>;
32
33
  /** Default foreground timeout in milliseconds. */
33
- timeoutMs?: number;
34
+ timeoutMs: Volatile<number>;
34
35
  /** Upper bound for per-call timeout overrides. */
35
- maxTimeoutMs?: number;
36
+ maxTimeoutMs: Volatile<number>;
36
37
  /** Per-stream in-memory output cap; overflow spills to a temp file. */
37
- maxOutputBytes?: number;
38
+ maxOutputBytes: Volatile<number>;
38
39
  /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
39
- maxSpillBytes?: number;
40
+ maxSpillBytes: Volatile<number>;
40
41
  /** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
41
- graceMs?: number;
42
+ graceMs: Volatile<number>;
42
43
  }
43
- /** The shape after schemastery applied the defaults (cwd has none). */
44
- type ResolvedConfig = Required<Omit<Config, 'cwd'>> & Pick<Config, 'cwd'>;
45
44
  /**
46
45
  * Reject a resolved section this executor could not run with. The schema
47
46
  * expresses neither "positive and finite" nor the timer bound `graceMs` has to
48
- * fit, so a stored value is refused where it is written instead of failing at
49
- * the next command.
50
- * @param config - the resolved section, schema-valid by construction.
47
+ * fit, so a stored value that cannot be used fails at the next command.
48
+ * @param config - the live configuration, schema-valid by construction.
51
49
  * @throws Error naming the field that cannot be used.
52
50
  */
53
51
  export declare function assertServiceableBashConfig(config: Config): void;
@@ -59,49 +57,48 @@ export declare function assertServiceableBashConfig(config: Config): void;
59
57
  * composition teardown) even across an executor reload.
60
58
  */
61
59
  export declare class LocalBashExecutor extends ShellExecutor {
60
+ readonly config: Config;
62
61
  static inject: string[];
63
- static Config: z<Config>;
64
- /** The currently authoritative config: the settings section, or the composition entry. */
65
- private source;
66
- /** Validated config (schemastery applied the defaults before construction). */
67
- get config(): ResolvedConfig;
62
+ static Config: z<Schemastery.ObjectS<NoInfer<{
63
+ cwd: z<string, string, "volatile">;
64
+ timeoutMs: z<number, number, "volatile-defined">;
65
+ maxTimeoutMs: z<number, number, "volatile-defined">;
66
+ maxOutputBytes: z<number, number, "volatile-defined">;
67
+ maxSpillBytes: z<number, number, "volatile-defined">;
68
+ graceMs: z<number, number, "volatile-defined">;
69
+ }>>, Schemastery.ObjectT<NoInfer<{
70
+ cwd: z<string, string, "volatile">;
71
+ timeoutMs: z<number, number, "volatile-defined">;
72
+ maxTimeoutMs: z<number, number, "volatile-defined">;
73
+ maxOutputBytes: z<number, number, "volatile-defined">;
74
+ maxSpillBytes: z<number, number, "volatile-defined">;
75
+ graceMs: z<number, number, "volatile-defined">;
76
+ }>>, "plain">;
68
77
  constructor(ctx: Context, config: Config);
69
78
  /**
70
79
  * Resolve a request into a fully-specified spec: fill `workdir` from
71
80
  * `config.cwd` (else `process.cwd()`), and `timeoutMs` from
72
81
  * `config.timeoutMs`, capped at `config.maxTimeoutMs`. The tool layer calls
73
- * this before {@link run}/{@link start}, so those methods receive explicit
74
- * values and never re-default.
82
+ * this before {@link execute}, so it receives explicit values and never
83
+ * re-defaults.
75
84
  */
76
85
  resolve(request: ShellExecRequest): ShellExecSpec;
77
86
  /** Map one resolved bash spec and explicit argv onto a fully-specified subprocess spawn. */
78
87
  private spawnSpec;
79
88
  /** The collect-mode readers the executor itself requested (present by construction). */
80
89
  private static collected;
81
- run(spec: ShellExecSpec): Promise<ShellRunResult>;
90
+ execute(spec: ShellExecSpec): Promise<ShellExecution>;
82
91
  /**
83
- * Run an explicit argv with the foreground lifecycle, environment, output,
84
- * timeout, and cancellation semantics of this executor. Subclasses use this
92
+ * Execute an explicit argv with the lifecycle, environment, output,
93
+ * deadline, and cancellation semantics of this executor. Subclasses use this
85
94
  * after replacing the public command's shell argv at an execution boundary.
86
95
  * @param spec - resolved execution settings and caller-owned command metadata.
87
- * @param argvOrPrepare - exact argv, or preparation cancelled by the same deadline as execution.
88
- * @returns the foreground result and whether argv reached the subprocess provider.
96
+ * @param argvOrPrepare - exact argv, or preparation using the execution cancellation signal.
97
+ * @param onStarted - installs provider facts synchronously before the handle can settle.
98
+ * @returns the live execution handle; spawn rejection settles the handle as
99
+ * killed while `result()` carries the same failure as its rejection.
89
100
  */
90
- protected runArgv(spec: ShellExecSpec, argvOrPrepare: readonly string[] | ((signal: AbortSignal) => Promise<readonly string[]>)): Promise<{
91
- result: ShellRunResult;
92
- spawnRequested: boolean;
93
- }>;
94
- start(spec: ShellExecSpec): Promise<ShellProcess>;
95
- /**
96
- * Start an explicit argv with the background lifecycle, environment, output,
97
- * cancellation, and managed-range ownership semantics of this executor.
98
- * Subclasses use this after replacing the public command's shell argv at an
99
- * execution boundary.
100
- * @param spec - resolved execution settings and caller-owned command metadata.
101
- * @param argv - exact executable and arguments to hand to `ctx.subprocess`.
102
- * @returns the live background handle; provider rejection settles it as killed.
103
- */
104
- protected startArgv(spec: ShellExecSpec, argv: readonly string[]): ShellProcess;
101
+ protected executeArgv(spec: ShellExecSpec, argvOrPrepare: readonly string[] | ((signal: AbortSignal) => Promise<readonly string[]>), onStarted?: (process: ShellExecution) => void): Promise<ShellExecution>;
105
102
  /**
106
103
  * Settlement hook for subclasses that attach execution facts to a process.
107
104
  * Called after exit facts or provider-failure output are stamped and before
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-bash-local",
3
3
  "description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,21 +27,19 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
31
- "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
32
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
33
- "@deepseek-ai/cordis": "^4.0.2",
34
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2"
30
+ "@deepseek-ai/dsh-shell": "^0.1.7-alpha.1",
31
+ "@deepseek-ai/dsh-subprocess": "^0.1.7-alpha.1",
32
+ "@deepseek-ai/dsh-timeout": "^0.1.7-alpha.1",
33
+ "@deepseek-ai/cordis": "^4.0.3"
35
34
  },
36
35
  "dependencies": {
37
- "@deepseek-ai/schemastery": "^3.18.2"
36
+ "@deepseek-ai/schemastery": "^3.18.3"
38
37
  },
39
38
  "devDependencies": {
40
- "@deepseek-ai/dsh-shell": "^0.1.6-alpha.2",
41
- "@deepseek-ai/dsh-subprocess-local": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-timeout": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
44
- "@deepseek-ai/cordis": "^4.0.2",
45
- "@deepseek-ai/dsh-settings": "^0.1.6-alpha.2"
39
+ "@deepseek-ai/dsh-shell": "^0.1.7-alpha.1",
40
+ "@deepseek-ai/dsh-subprocess": "^0.1.7-alpha.1",
41
+ "@deepseek-ai/dsh-subprocess-local": "^0.1.7-alpha.1",
42
+ "@deepseek-ai/dsh-timeout": "^0.1.7-alpha.1",
43
+ "@deepseek-ai/cordis": "^4.0.3"
46
44
  }
47
45
  }