@deepseek-ai/dsh-bash-local 0.0.1-rc.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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +46 -0
- package/README.zh.md +46 -0
- package/lib/index.js +308 -0
- package/lib/invariant.js +23 -0
- package/lib/types/index.d.ts +104 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/bash/bash-local/README.md
|
|
5
|
+
README.md: cb8e7f0ae766d9b1c5f1678e77d35992085d3d52
|
|
6
|
+
README.zh.md: 20af9c18998c6f3f0403c50f3a8ac599607dc094
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-bash-local
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
Local Service provider for the `@deepseek-ai/dsh-bash` executor seam over the [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) service: `LocalBashExecutor` spawns `bash -c <command>` per call as a managed process group through `ctx.subprocess`, and owns everything bash-shaped — command defaulting and caps, timeout/cancel classification, the model-friendly terminal environment, and the model-facing stdout/stderr merge for background reads. Group mechanics (bounded spill-backed output, credential scrub, kill escalation, disposal) are the subprocess service's.
|
|
6
|
+
|
|
7
|
+
The package root exports the default and named `LocalBashExecutor` plugin plus its `Config`.
|
|
8
|
+
|
|
9
|
+
## Config
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
- id: bash
|
|
13
|
+
name: '@deepseek-ai/dsh-bash-local'
|
|
14
|
+
config:
|
|
15
|
+
cwd: /path/to/workspace # default: process.cwd()
|
|
16
|
+
timeoutMs: 120000 # default foreground timeout
|
|
17
|
+
maxTimeoutMs: 600000 # cap for per-call overrides
|
|
18
|
+
maxOutputBytes: 64000 # per-stream in-memory cap; overflow spills to disk
|
|
19
|
+
maxSpillBytes: 67108864 # per-stream full-output spill cap
|
|
20
|
+
graceMs: 3000 # kill escalation and post-exit pipe-drain grace
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Behavior
|
|
24
|
+
|
|
25
|
+
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` with no rc files.
|
|
26
|
+
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. The grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so Node can represent it with one timer. Process-group kills, post-exit pipe draining, tail retention, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
|
|
27
|
+
- **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-signaled command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
|
|
28
|
+
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` prevents pagers and ANSI color from garbling results. These values merge as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
|
29
|
+
- **Background processes** — `start()` returns a live `BashProcess` handle immediately with no timeout, and `readOutput()` merges offset-based stdout/stderr reads into one consuming delta, placing stderr under a `[stderr]` marker when present. A running process belongs to the subprocess service, survives executor reloads, and is killed and joined on service disposal. Task ids, ownership, polling, and notices belong to the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with.
|
|
30
|
+
|
|
31
|
+
## Model Experience
|
|
32
|
+
|
|
33
|
+
Indirectly, through `dsh-tool-bash`, which renders this executor's bounded stdout/stderr tails, background-process deltas, spill-file paths, and infrastructure failures.
|
|
34
|
+
|
|
35
|
+
#### KV Cache effect
|
|
36
|
+
|
|
37
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
38
|
+
|
|
39
|
+
## Known Limitations and Deferred Work
|
|
40
|
+
|
|
41
|
+
- **Unconfined by itself** — this executor always runs commands with the harness process's authority; deployments needing confinement compose [`dsh-bash-sandbox`](../bash-sandbox/README.md), while per-call allow/deny/ask policy belongs on `tools/pre-execute`.
|
|
42
|
+
- **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.
|
|
43
|
+
- **POSIX-only** — the `bash` binary is hardcoded, and the underlying service's group semantics are POSIX; Windows is unsupported.
|
|
44
|
+
- **A background spawn-failure note is single-delivery** — the subprocess service buffers no output for a process that never ran, so the executor injects `spawn failed: …` into exactly one `readOutput()` delta; a reader that discards that delta cannot recover it.
|
|
45
|
+
|
|
46
|
+
Scrub-heuristic and spill-retention caveats live with [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md), which owns those mechanics.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-bash-local
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
`@deepseek-ai/dsh-bash` 执行器 seam 的本地 Service provider,构建在 [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) 服务之上:`LocalBashExecutor` 每次调用都通过 `ctx.subprocess` 把 `bash -c <command>` 作为受管进程组 spawn,并负责所有 Bash 层职责(命令默认值补全与上限、超时与取消分类、适合模型的终端环境,以及后台读取时面向模型的 stdout/stderr 合并)。以 spill 文件兜底的有界输出、凭据清除、kill 升级和 dispose(资源释放)等进程组机制则由 subprocess 服务负责。
|
|
6
|
+
|
|
7
|
+
包根目录导出默认与具名的 `LocalBashExecutor` 插件及其 `Config`。
|
|
8
|
+
|
|
9
|
+
## 配置
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
- id: bash
|
|
13
|
+
name: '@deepseek-ai/dsh-bash-local'
|
|
14
|
+
config:
|
|
15
|
+
cwd: /path/to/workspace # default: process.cwd()
|
|
16
|
+
timeoutMs: 120000 # default foreground timeout
|
|
17
|
+
maxTimeoutMs: 600000 # cap for per-call overrides
|
|
18
|
+
maxOutputBytes: 64000 # per-stream in-memory cap; overflow spills to disk
|
|
19
|
+
maxSpillBytes: 67108864 # per-stream full-output spill cap
|
|
20
|
+
graceMs: 3000 # kill escalation and post-exit pipe-drain grace
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 行为
|
|
24
|
+
|
|
25
|
+
- **每次调用都 spawn,不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`,且不读取 rc 文件。
|
|
26
|
+
- **在受管进程组之上应用配置预算**:`resolve()` 从配置补全 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算;stderr 和后台运行仍使用 `maxOutputBytes`。
|
|
27
|
+
- **超时与取消分类**:`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。
|
|
28
|
+
- **适合模型的终端环境**:`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
|
|
29
|
+
- **后台进程**:`start()` 会立即返回活动的 `BashProcess` 句柄且不应用超时;`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行中的进程属于 subprocess 服务,可在执行器重载后存活,并在服务 dispose 时被终止且等待退出。task id、所有权、轮询和通知属于通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md),工具层会在其中注册该句柄。
|
|
30
|
+
|
|
31
|
+
## 模型体验
|
|
32
|
+
|
|
33
|
+
通过 `dsh-tool-bash` 间接影响;该工具会渲染此执行器有界的 stdout/stderr 尾部、后台进程增量、spill 文件路径与基础设施失败。
|
|
34
|
+
|
|
35
|
+
#### KV Cache 影响
|
|
36
|
+
|
|
37
|
+
不会直接导致 KV Cache 失效;请求前缀变更由具名消费方负责。
|
|
38
|
+
|
|
39
|
+
## 已知限制与暂缓事项
|
|
40
|
+
|
|
41
|
+
- **自身不提供隔离**:此执行器始终以 harness 进程的权限运行命令;需要限制的部署可以组合 [`dsh-bash-sandbox`](../bash-sandbox/README.md),每次调用的 allow/deny/ask 策略则属于 `tools/pre-execute`。
|
|
42
|
+
- **没有持久 shell 或 PTY**:每次调用都启动新的非登录 `bash -c`;仅持久化 cwd 与交互式终端会话均继续暂缓,直到真实工作流需要它们。
|
|
43
|
+
- **仅支持 POSIX**:`bash` 二进制已硬编码,底层服务的进程组语义也是 POSIX 的;不支持 Windows。
|
|
44
|
+
- **后台 spawn 失败提示只交付一次**:进程管理器不会为从未真正运行的进程缓冲任何输出,因此执行器把 `spawn failed: …` 注入恰好一个 `readOutput()` 增量;丢弃了该增量的读取方无法再恢复它。
|
|
45
|
+
|
|
46
|
+
凭据清除启发式规则与 spill 保留的注意事项随 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 记录;这些机制归它所有。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { BashExecutor } from "@deepseek-ai/dsh-bash";
|
|
3
|
+
import { MAX_TIMER_DELAY_MS, clampTimeout, deadline, timeoutOf } from "@deepseek-ai/dsh-timeout";
|
|
4
|
+
//#region lib/types/index.js
|
|
5
|
+
/**
|
|
6
|
+
* Local Service provider for the bash capability seam over the subprocess
|
|
7
|
+
* capability seam. Public commands run as `bash -c` in a managed process group spawned
|
|
8
|
+
* through `ctx.subprocess`; subclasses may reuse the same mechanics with an
|
|
9
|
+
* explicit argv. This executor owns command defaulting, deadlines and cause
|
|
10
|
+
* classification, the model-friendly terminal environment, and the model-facing
|
|
11
|
+
* stdout/stderr merge for background reads. Execution policy belongs in
|
|
12
|
+
* `tools/pre-execute` or a sandboxing executor.
|
|
13
|
+
* @module @deepseek-ai/dsh-bash-local
|
|
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
|
+
/**
|
|
74
|
+
* Model-friendly environment overrides: disable colors, pagers, and
|
|
75
|
+
* interactive terminal features that would garble tool output (the same set
|
|
76
|
+
* Codex hardcodes; Claude Code achieves it via TERM=dumb). Bash-tool policy —
|
|
77
|
+
* merged first into the spawn's explicit env, so a trusted caller's own entry
|
|
78
|
+
* still wins; the subprocess service applies its credential scrub independently.
|
|
79
|
+
*/
|
|
80
|
+
const ENV_OVERRIDES = {
|
|
81
|
+
NO_COLOR: "1",
|
|
82
|
+
TERM: "dumb",
|
|
83
|
+
PAGER: "cat",
|
|
84
|
+
GIT_PAGER: "cat"
|
|
85
|
+
};
|
|
86
|
+
/** Default SIGTERM→SIGKILL grace period (the `graceMs` config; matches OpenCode's 3s). */
|
|
87
|
+
const DEFAULT_GRACE_MS = 3e3;
|
|
88
|
+
/** Default per-stream spill cap (the `maxSpillBytes` config). */
|
|
89
|
+
const DEFAULT_MAX_SPILL_BYTES = 64 * 1024 * 1024;
|
|
90
|
+
/** Project a settled collect-mode reader into the final CollectedOutput shape. */
|
|
91
|
+
function finalOutput(reader) {
|
|
92
|
+
const read = reader.readFrom(0);
|
|
93
|
+
return {
|
|
94
|
+
text: read.text,
|
|
95
|
+
truncated: read.lossy,
|
|
96
|
+
...read.spillPath !== void 0 ? { spillPath: read.spillPath } : {}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function assertPositiveFinite(name, value) {
|
|
100
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`bash-local: ${name} must be a positive finite number`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Local bash executor over `ctx.subprocess`. Bounded output, spill files, and
|
|
104
|
+
* process-group SIGTERM→SIGKILL escalation are the subprocess service's
|
|
105
|
+
* mechanics; this executor supplies their configured budgets per spawn, so a
|
|
106
|
+
* still-running background process stays managed (killed and joined at
|
|
107
|
+
* composition teardown) even across an executor reload.
|
|
108
|
+
*/
|
|
109
|
+
var LocalBashExecutor = class LocalBashExecutor extends BashExecutor {
|
|
110
|
+
static inject = ["subprocess"];
|
|
111
|
+
static Config = z.object({
|
|
112
|
+
cwd: z.string(),
|
|
113
|
+
timeoutMs: z.number().default(12e4),
|
|
114
|
+
maxTimeoutMs: z.number().default(6e5),
|
|
115
|
+
maxOutputBytes: z.number().default(64e3),
|
|
116
|
+
maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES),
|
|
117
|
+
graceMs: z.number().default(DEFAULT_GRACE_MS)
|
|
118
|
+
});
|
|
119
|
+
/** Validated config (schemastery applied the defaults before construction). */
|
|
120
|
+
config;
|
|
121
|
+
constructor(ctx, config) {
|
|
122
|
+
super(ctx);
|
|
123
|
+
this.config = config;
|
|
124
|
+
assertPositiveFinite("timeoutMs", this.config.timeoutMs);
|
|
125
|
+
assertPositiveFinite("maxTimeoutMs", this.config.maxTimeoutMs);
|
|
126
|
+
assertPositiveFinite("maxOutputBytes", this.config.maxOutputBytes);
|
|
127
|
+
assertPositiveFinite("maxSpillBytes", this.config.maxSpillBytes);
|
|
128
|
+
assertPositiveFinite("graceMs", this.config.graceMs);
|
|
129
|
+
if (this.config.graceMs > MAX_TIMER_DELAY_MS) throw new Error(`bash-local: graceMs must be no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a request into a fully-specified spec: fill `workdir` from
|
|
133
|
+
* `config.cwd` (else `process.cwd()`), and `timeoutMs` from
|
|
134
|
+
* `config.timeoutMs`, capped at `config.maxTimeoutMs`. The tool layer calls
|
|
135
|
+
* this before {@link run}/{@link start}, so those methods receive explicit
|
|
136
|
+
* values and never re-default.
|
|
137
|
+
*/
|
|
138
|
+
resolve(request) {
|
|
139
|
+
const timeoutMs = clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, "bash-local: request.timeoutMs");
|
|
140
|
+
const stdoutMaxBytes = request.stdoutMaxBytes ?? this.config.maxOutputBytes;
|
|
141
|
+
assertPositiveFinite("request.stdoutMaxBytes", stdoutMaxBytes);
|
|
142
|
+
return {
|
|
143
|
+
command: request.command,
|
|
144
|
+
workdir: request.workdir ?? this.config.cwd ?? process.cwd(),
|
|
145
|
+
timeoutMs,
|
|
146
|
+
stdoutMaxBytes,
|
|
147
|
+
...request.signal ? { signal: request.signal } : {},
|
|
148
|
+
...request.stdin !== void 0 ? { stdin: request.stdin } : {},
|
|
149
|
+
...request.env !== void 0 ? { env: request.env } : {},
|
|
150
|
+
...request.dshEnv !== void 0 ? { dshEnv: request.dshEnv } : {},
|
|
151
|
+
sandboxPolicy: request.sandboxPolicy
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Map one resolved bash spec and explicit argv onto a fully-specified subprocess spawn. */
|
|
155
|
+
spawnSpec(spec, argv, stdoutMaxBytes, signal) {
|
|
156
|
+
const collect = (maxBytes) => ({
|
|
157
|
+
maxBytes,
|
|
158
|
+
spill: { maxBytes: this.config.maxSpillBytes }
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
argv,
|
|
162
|
+
cwd: spec.workdir,
|
|
163
|
+
stdio: {
|
|
164
|
+
stdin: spec.stdin !== void 0 ? { data: spec.stdin } : "ignore",
|
|
165
|
+
stdout: collect(stdoutMaxBytes),
|
|
166
|
+
stderr: collect(this.config.maxOutputBytes)
|
|
167
|
+
},
|
|
168
|
+
graceMs: this.config.graceMs,
|
|
169
|
+
signal,
|
|
170
|
+
env: {
|
|
171
|
+
...ENV_OVERRIDES,
|
|
172
|
+
...spec.env,
|
|
173
|
+
...spec.dshEnv
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** The collect-mode readers the executor itself requested (present by construction). */
|
|
178
|
+
static collected(handle) {
|
|
179
|
+
const { stdout, stderr } = handle.collected;
|
|
180
|
+
/* v8 ignore start -- collect dispositions expose both readers by the seam contract; defensive. */
|
|
181
|
+
if (stdout === void 0 || stderr === void 0) throw new Error("bash-local: subprocess implementation dropped a requested collect stream");
|
|
182
|
+
/* v8 ignore stop */
|
|
183
|
+
return {
|
|
184
|
+
stdout,
|
|
185
|
+
stderr
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async run(spec) {
|
|
189
|
+
return this.runArgv(spec, [
|
|
190
|
+
"bash",
|
|
191
|
+
"-c",
|
|
192
|
+
spec.command
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Run an explicit argv with the foreground lifecycle, environment, output,
|
|
197
|
+
* timeout, and cancellation semantics of this executor. Subclasses use this
|
|
198
|
+
* after replacing the public command's shell argv at an execution boundary.
|
|
199
|
+
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
200
|
+
* @param argv - exact executable and arguments to hand to `ctx.subprocess`.
|
|
201
|
+
* @returns the settled foreground result with collected output and cause facts.
|
|
202
|
+
*/
|
|
203
|
+
async runArgv(spec, argv) {
|
|
204
|
+
const env_1 = {
|
|
205
|
+
stack: [],
|
|
206
|
+
error: void 0,
|
|
207
|
+
hasError: false
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
const d = __addDisposableResource(env_1, deadline(spec.signal, spec.timeoutMs, "BASH_TIMEOUT"), false);
|
|
211
|
+
const handle = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, spec.stdoutMaxBytes, d.signal));
|
|
212
|
+
const outcome = await handle.done;
|
|
213
|
+
const collected = LocalBashExecutor.collected(handle);
|
|
214
|
+
const timedOut = timeoutOf(d.signal, "BASH_TIMEOUT") !== void 0;
|
|
215
|
+
const aborted = d.signal.aborted && !timedOut;
|
|
216
|
+
return {
|
|
217
|
+
...outcome,
|
|
218
|
+
timedOut,
|
|
219
|
+
aborted,
|
|
220
|
+
timeoutMs: spec.timeoutMs,
|
|
221
|
+
stdout: finalOutput(collected.stdout),
|
|
222
|
+
stderr: finalOutput(collected.stderr)
|
|
223
|
+
};
|
|
224
|
+
} catch (e_1) {
|
|
225
|
+
env_1.error = e_1;
|
|
226
|
+
env_1.hasError = true;
|
|
227
|
+
} finally {
|
|
228
|
+
__disposeResources(env_1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
start(spec) {
|
|
232
|
+
return this.startArgv(spec, [
|
|
233
|
+
"bash",
|
|
234
|
+
"-c",
|
|
235
|
+
spec.command
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Start an explicit argv with the background lifecycle, environment, output,
|
|
240
|
+
* cancellation, and process-tree ownership semantics of this executor.
|
|
241
|
+
* Subclasses use this after replacing the public command's shell argv at an
|
|
242
|
+
* execution boundary.
|
|
243
|
+
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
244
|
+
* @param argv - exact executable and arguments to hand to `ctx.subprocess`.
|
|
245
|
+
* @returns the live background handle; spawn rejection settles it as killed.
|
|
246
|
+
*/
|
|
247
|
+
startArgv(spec, argv) {
|
|
248
|
+
const running = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, this.config.maxOutputBytes, spec.signal));
|
|
249
|
+
const collected = LocalBashExecutor.collected(running);
|
|
250
|
+
let spawnFailureNote;
|
|
251
|
+
const consumeSpawnFailure = () => {
|
|
252
|
+
const note = spawnFailureNote ?? "";
|
|
253
|
+
spawnFailureNote = void 0;
|
|
254
|
+
return note;
|
|
255
|
+
};
|
|
256
|
+
let stdoutOffset = 0;
|
|
257
|
+
let stderrOffset = 0;
|
|
258
|
+
const proc = {
|
|
259
|
+
status: "running",
|
|
260
|
+
exitCode: null,
|
|
261
|
+
signal: null,
|
|
262
|
+
done: running.done.then((outcome) => {
|
|
263
|
+
if (proc.status === "running") proc.status = spec.signal?.aborted === true || outcome.signal !== null ? "killed" : "completed";
|
|
264
|
+
proc.exitCode = outcome.exitCode;
|
|
265
|
+
proc.signal = outcome.signal;
|
|
266
|
+
this.onProcessDone(proc, collected.stderr.readFrom(0).text, false);
|
|
267
|
+
}, (error) => {
|
|
268
|
+
proc.status = "killed";
|
|
269
|
+
spawnFailureNote = `spawn failed: ${String(error)}`;
|
|
270
|
+
this.onProcessDone(proc, spawnFailureNote, true, error);
|
|
271
|
+
}),
|
|
272
|
+
readOutput: () => {
|
|
273
|
+
const out = collected.stdout.readFrom(stdoutOffset);
|
|
274
|
+
const err = collected.stderr.readFrom(stderrOffset);
|
|
275
|
+
stdoutOffset = out.nextOffset;
|
|
276
|
+
stderrOffset = err.nextOffset;
|
|
277
|
+
const errText = err.text.length > 0 ? err.text : consumeSpawnFailure();
|
|
278
|
+
const separator = out.text.length > 0 && !out.text.endsWith("\n") ? "\n" : "";
|
|
279
|
+
return {
|
|
280
|
+
delta: out.text + (errText.length > 0 ? `${separator}[stderr]\n${errText}` : ""),
|
|
281
|
+
lossy: out.lossy || err.lossy,
|
|
282
|
+
...out.spillPath !== void 0 ? { stdoutSpillPath: out.spillPath } : {},
|
|
283
|
+
...err.spillPath !== void 0 ? { stderrSpillPath: err.spillPath } : {}
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
kill: () => {
|
|
287
|
+
if (proc.status !== "running") return false;
|
|
288
|
+
proc.status = "killed";
|
|
289
|
+
running.terminate();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return proc;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Settlement hook for subclasses that attach execution facts to a process.
|
|
297
|
+
* Called after exit facts or spawn-failure output are stamped and before
|
|
298
|
+
* {@link BashProcess.done} resolves. The base implementation is intentionally
|
|
299
|
+
* empty.
|
|
300
|
+
* @param _proc - the settled process handle.
|
|
301
|
+
* @param _stderr - the process's retained stderr tail used by subclasses for settlement classification.
|
|
302
|
+
* @param _spawnFailed - whether the subprocess promise rejected before a process started.
|
|
303
|
+
* @param _spawnError - the original spawn rejection reason, which may itself be undefined.
|
|
304
|
+
*/
|
|
305
|
+
onProcessDone(_proc, _stderr, _spawnFailed, _spawnError) {}
|
|
306
|
+
};
|
|
307
|
+
//#endregion
|
|
308
|
+
export { ENV_OVERRIDES, LocalBashExecutor, LocalBashExecutor as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-bash-local`.
|
|
4
|
+
* @module @deepseek-ai/dsh-bash-local/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-bash-local";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "bash-local-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Service provider for the bash capability seam over the subprocess
|
|
3
|
+
* capability seam. Public commands run as `bash -c` in a managed process group spawned
|
|
4
|
+
* through `ctx.subprocess`; subclasses may reuse the same mechanics with an
|
|
5
|
+
* explicit argv. This executor owns command defaulting, deadlines and cause
|
|
6
|
+
* classification, the model-friendly terminal environment, and the model-facing
|
|
7
|
+
* stdout/stderr merge for background reads. Execution policy belongs in
|
|
8
|
+
* `tools/pre-execute` or a sandboxing executor.
|
|
9
|
+
* @module @deepseek-ai/dsh-bash-local
|
|
10
|
+
*/
|
|
11
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import z from '@deepseek-ai/schemastery';
|
|
13
|
+
import { BashExecutor } from '@deepseek-ai/dsh-bash';
|
|
14
|
+
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash';
|
|
15
|
+
/**
|
|
16
|
+
* Model-friendly environment overrides: disable colors, pagers, and
|
|
17
|
+
* interactive terminal features that would garble tool output (the same set
|
|
18
|
+
* Codex hardcodes; Claude Code achieves it via TERM=dumb). Bash-tool policy —
|
|
19
|
+
* merged first into the spawn's explicit env, so a trusted caller's own entry
|
|
20
|
+
* still wins; the subprocess service applies its credential scrub independently.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ENV_OVERRIDES: {
|
|
23
|
+
readonly NO_COLOR: "1";
|
|
24
|
+
readonly TERM: "dumb";
|
|
25
|
+
readonly PAGER: "cat";
|
|
26
|
+
readonly GIT_PAGER: "cat";
|
|
27
|
+
};
|
|
28
|
+
/** Plugin config (all optional — `static Config` supplies the defaults). */
|
|
29
|
+
export interface Config {
|
|
30
|
+
/** Default working directory for commands (default: process.cwd()). */
|
|
31
|
+
cwd?: string;
|
|
32
|
+
/** Default foreground timeout in milliseconds. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
/** Upper bound for per-call timeout overrides. */
|
|
35
|
+
maxTimeoutMs?: number;
|
|
36
|
+
/** Per-stream in-memory output cap; overflow spills to a temp file. */
|
|
37
|
+
maxOutputBytes?: number;
|
|
38
|
+
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
|
|
39
|
+
maxSpillBytes?: number;
|
|
40
|
+
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
|
|
41
|
+
graceMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/** The shape after schemastery applied the defaults (cwd has none). */
|
|
44
|
+
type ResolvedConfig = Required<Omit<Config, 'cwd'>> & Pick<Config, 'cwd'>;
|
|
45
|
+
/**
|
|
46
|
+
* Local bash executor over `ctx.subprocess`. Bounded output, spill files, and
|
|
47
|
+
* process-group SIGTERM→SIGKILL escalation are the subprocess service's
|
|
48
|
+
* mechanics; this executor supplies their configured budgets per spawn, so a
|
|
49
|
+
* still-running background process stays managed (killed and joined at
|
|
50
|
+
* composition teardown) even across an executor reload.
|
|
51
|
+
*/
|
|
52
|
+
export declare class LocalBashExecutor extends BashExecutor {
|
|
53
|
+
static inject: string[];
|
|
54
|
+
static Config: z<Config>;
|
|
55
|
+
/** Validated config (schemastery applied the defaults before construction). */
|
|
56
|
+
readonly config: ResolvedConfig;
|
|
57
|
+
constructor(ctx: Context, config: Config);
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a request into a fully-specified spec: fill `workdir` from
|
|
60
|
+
* `config.cwd` (else `process.cwd()`), and `timeoutMs` from
|
|
61
|
+
* `config.timeoutMs`, capped at `config.maxTimeoutMs`. The tool layer calls
|
|
62
|
+
* this before {@link run}/{@link start}, so those methods receive explicit
|
|
63
|
+
* values and never re-default.
|
|
64
|
+
*/
|
|
65
|
+
resolve(request: BashExecRequest): BashExecSpec;
|
|
66
|
+
/** Map one resolved bash spec and explicit argv onto a fully-specified subprocess spawn. */
|
|
67
|
+
private spawnSpec;
|
|
68
|
+
/** The collect-mode readers the executor itself requested (present by construction). */
|
|
69
|
+
private static collected;
|
|
70
|
+
run(spec: BashExecSpec): Promise<BashRunResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Run an explicit argv with the foreground lifecycle, environment, output,
|
|
73
|
+
* timeout, and cancellation semantics of this executor. Subclasses use this
|
|
74
|
+
* after replacing the public command's shell argv at an execution boundary.
|
|
75
|
+
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
76
|
+
* @param argv - exact executable and arguments to hand to `ctx.subprocess`.
|
|
77
|
+
* @returns the settled foreground result with collected output and cause facts.
|
|
78
|
+
*/
|
|
79
|
+
protected runArgv(spec: BashExecSpec, argv: readonly string[]): Promise<BashRunResult>;
|
|
80
|
+
start(spec: BashExecSpec): BashProcess;
|
|
81
|
+
/**
|
|
82
|
+
* Start an explicit argv with the background lifecycle, environment, output,
|
|
83
|
+
* cancellation, and process-tree ownership semantics of this executor.
|
|
84
|
+
* Subclasses use this after replacing the public command's shell argv at an
|
|
85
|
+
* execution boundary.
|
|
86
|
+
* @param spec - resolved execution settings and caller-owned command metadata.
|
|
87
|
+
* @param argv - exact executable and arguments to hand to `ctx.subprocess`.
|
|
88
|
+
* @returns the live background handle; spawn rejection settles it as killed.
|
|
89
|
+
*/
|
|
90
|
+
protected startArgv(spec: BashExecSpec, argv: readonly string[]): BashProcess;
|
|
91
|
+
/**
|
|
92
|
+
* Settlement hook for subclasses that attach execution facts to a process.
|
|
93
|
+
* Called after exit facts or spawn-failure output are stamped and before
|
|
94
|
+
* {@link BashProcess.done} resolves. The base implementation is intentionally
|
|
95
|
+
* empty.
|
|
96
|
+
* @param _proc - the settled process handle.
|
|
97
|
+
* @param _stderr - the process's retained stderr tail used by subclasses for settlement classification.
|
|
98
|
+
* @param _spawnFailed - whether the subprocess promise rejected before a process started.
|
|
99
|
+
* @param _spawnError - the original spawn rejection reason, which may itself be undefined.
|
|
100
|
+
*/
|
|
101
|
+
protected onProcessDone(_proc: BashProcess, _stderr: string, _spawnFailed: boolean, _spawnError?: unknown): void;
|
|
102
|
+
}
|
|
103
|
+
export default LocalBashExecutor;
|
|
104
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-bash-local`.
|
|
3
|
+
* @module @deepseek-ai/dsh-bash-local/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "bash-local-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-bash-local",
|
|
3
|
+
"description": "Local-subprocess implementation of the DeepSeek Harness bash executor seam",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/bash/bash-local"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "BSD-3-Clause",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh-bash": "^0.0.1-rc.1",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
37
|
+
"@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
|
|
38
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-bash": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-subprocess": "^0.0.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-timeout": "^0.0.1-rc.1",
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1"
|
|
51
|
+
}
|
|
52
|
+
}
|