@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 +2 -2
- package/README.md +7 -6
- package/README.zh.md +7 -6
- package/lib/index.js +153 -202
- package/lib/types/index.d.ts +37 -40
- package/package.json +11 -13
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:
|
|
6
|
-
README.zh.md:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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); `
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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()`
|
|
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
|
-
-
|
|
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 {
|
|
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
|
|
106
|
-
* the
|
|
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
|
-
|
|
112
|
-
assertPositiveFinite("
|
|
113
|
-
assertPositiveFinite("
|
|
114
|
-
assertPositiveFinite("
|
|
115
|
-
assertPositiveFinite("
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
162
|
-
*
|
|
85
|
+
* this before {@link execute}, so it receives explicit values and never
|
|
86
|
+
* re-defaults.
|
|
163
87
|
*/
|
|
164
88
|
resolve(request) {
|
|
165
|
-
|
|
166
|
-
const
|
|
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
|
|
215
|
-
return
|
|
140
|
+
async execute(spec) {
|
|
141
|
+
return this.executeArgv(spec, [
|
|
216
142
|
"bash",
|
|
217
143
|
"-c",
|
|
218
144
|
spec.command
|
|
219
|
-
])
|
|
145
|
+
]);
|
|
220
146
|
}
|
|
221
147
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
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
|
|
227
|
-
* @
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
168
|
+
aborted: d.signal.aborted && !timedOut
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
disarm = () => {
|
|
172
|
+
d[Symbol.dispose]();
|
|
289
173
|
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
174
|
+
} else {
|
|
175
|
+
spawnSignal = spec.signal;
|
|
176
|
+
classify = () => ({
|
|
177
|
+
timedOut: false,
|
|
178
|
+
aborted: spec.signal?.aborted === true
|
|
179
|
+
});
|
|
295
180
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
|
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
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
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
|
|
32
|
+
cwd: Volatile<string | undefined>;
|
|
32
33
|
/** Default foreground timeout in milliseconds. */
|
|
33
|
-
timeoutMs
|
|
34
|
+
timeoutMs: Volatile<number>;
|
|
34
35
|
/** Upper bound for per-call timeout overrides. */
|
|
35
|
-
maxTimeoutMs
|
|
36
|
+
maxTimeoutMs: Volatile<number>;
|
|
36
37
|
/** Per-stream in-memory output cap; overflow spills to a temp file. */
|
|
37
|
-
maxOutputBytes
|
|
38
|
+
maxOutputBytes: Volatile<number>;
|
|
38
39
|
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
|
39
|
-
maxSpillBytes
|
|
40
|
+
maxSpillBytes: Volatile<number>;
|
|
40
41
|
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
|
41
|
-
graceMs
|
|
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
|
|
49
|
-
* the
|
|
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<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
74
|
-
*
|
|
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
|
-
|
|
90
|
+
execute(spec: ShellExecSpec): Promise<ShellExecution>;
|
|
82
91
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
88
|
-
* @
|
|
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
|
|
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.
|
|
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.
|
|
31
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.
|
|
32
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
33
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
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.
|
|
36
|
+
"@deepseek-ai/schemastery": "^3.18.3"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@deepseek-ai/dsh-shell": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-subprocess
|
|
42
|
-
"@deepseek-ai/dsh-
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
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
|
}
|