dsh-gitbash-shell 0.39.0 → 0.39.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/CHANGELOG.md +40 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -2
- package/src/bash-tool.js +2 -2
- package/src/fastctx-bridge.js +49 -10
- package/src/inline-tools.js +40 -16
- package/src/shell.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.39.1 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
**类型**:fix(route B 私有执行器的真实服务隔离与读取授权) / test / ci。
|
|
9
|
+
|
|
10
|
+
- **根因**:普通 `ctx.plugin()` 子 fiber 只隔离副作用所有权,不隔离服务命名空间。
|
|
11
|
+
`GitBashSandboxExecutor` 仍经 `SandboxBashExecutor → LocalBashExecutor → ShellExecutor → Service`
|
|
12
|
+
注册 `shell`,与 route B 保留的 `SandboxPwshExecutor` 冲突,报
|
|
13
|
+
`service "shell" has been registered at <SandboxPwshExecutor>`;bash 工具尚未注册即中断。
|
|
14
|
+
Git Bash 路径已成功解析时同样发生,不是工具发现或路径探测问题。
|
|
15
|
+
- **修复**:`mountPrivateExecutor` 仅对 `shell` 调用 `ctx.isolate('shell')`,其余宿主依赖原样继承;
|
|
16
|
+
类插件仍走官方 `plugin()` 通道,Config/volatile/static inject 与生命周期不变。
|
|
17
|
+
空 scope 通过 `get('shell')` 读取活跃私有实例,工具就绪检查用 `get('tools')`,避免
|
|
18
|
+
缺少 inject 声明的属性访问再度中断补位。失败时销毁已建 scope,成功/失败均清理挂载计时器。
|
|
19
|
+
- **边界**:不撤官方 PowerShell 执行器、不改 bundle patch、不改 shell 执行/沙箱/后台协议,
|
|
20
|
+
不抬 Core 兼容下限,不发版、不触发 npm 发布。
|
|
21
|
+
- **回归**:新增 5 项真实 Cordis 测试,覆盖普通子 scope 冲突、显式隔离后的 Config/inject 与
|
|
22
|
+
根 shell 保留、卸载、构造失败清理/再挂载、缺服务失败与挂载超时清理;不是仅正则源码检查。
|
|
23
|
+
既有 minimal PTY 测试的非 Windows 断言现在显式模拟 Linux,不再在恢复 win32 后误断言 `/bin/bash`。
|
|
24
|
+
首轮远端 CI 发现两项独立 error-content 测试误嵌套在父测试中,Node 22 会取消未等待的子测试;
|
|
25
|
+
恢复独立结构并保留全部断言,不跳过测试、不移除 Node 22 矩阵。
|
|
26
|
+
- **验证入口**:`npm test` 合并 smoke 与真实 Cordis 回归;新增 `npm run test:preview`,通过
|
|
27
|
+
显式 `DSH_PREVIEW_HOST_DIR` 只读解析目标宿主包,在新 Context + 临时 cwd 做无头运行时 preview。
|
|
28
|
+
Windows 本机已通过:Core **0.2.1-alpha.2**,宿主 Cordis **4.0.5-alpha.1**,Node **24.13.0**;
|
|
29
|
+
原路径真实重现服务冲突,修复后 bash 出现在实际 ToolRuntime schema 中并执行
|
|
30
|
+
`command -v bash`/`uname -s`/sentinel(退出码 0),官方 pwsh 执行 `Get-Process`,
|
|
31
|
+
`pwshTool` 开/关两态与卸载恢复均通过。**不等同于完整 GUI、PTY、后台任务或受限模式验收**。
|
|
32
|
+
- **CI**:新增非发布 `ci` 工作流(push 修复分支 / PR / 手动触发),Windows/Linux × Node 22/24,
|
|
33
|
+
锁定依赖、禁止生命周期脚本与可选载荷、完整测试及 `npm pack --dry-run --ignore-scripts`。
|
|
34
|
+
本机完整测试 **127/127 通过**,打包预检与 `git diff --check` 通过;远端矩阵结果见对应 PR 检查。
|
|
35
|
+
复验命令和覆盖边界见 `tests/README.md`。
|
|
36
|
+
- **gb_glob 守卫封杀了上游主用法([#21](https://github.com/KannaKuron/dsh-gitbash-shell/issues/21),同日报告)**:
|
|
37
|
+
上游 `tools/list` 的 glob `pattern` 是 `string | string[]` 的 untagged enum——**数组才是主形态**
|
|
38
|
+
(schema 示例本身就是数组),v0.38.5 的只认字符串守卫把它一并拒绝;且模型常把数组 JSON 序列化后
|
|
39
|
+
传入(实测 25 次调用 24 次为 `["*.txt"]` 字符串),上游按**一个字面 glob** 匹配 → 静默返回空
|
|
40
|
+
——恰好是守卫要防的「静默错误答案」。修法(`validateFastctxPattern`,提取为纯导出函数):
|
|
41
|
+
glob 接受非空字符串或非空字符串数组;**字符串化数组响亮报错并给出正确形状**(绝不代模型反序列化
|
|
42
|
+
——那会掩盖错误);replace 维持单正则字符串契约;其他类型报错带类型名。gb_glob 的 description
|
|
43
|
+
增补 pattern 形状声明(模型侧修正源)。**教训**:给上游加防御守卫必须从 `tools/list` 的
|
|
44
|
+
`inputSchema` 原文出发,单一探针会误判(v0.38.5 的「静默当全部文件」实测即单一探针结论)。
|
|
45
|
+
|
|
6
46
|
## v0.39.0 — 2026-10-13
|
|
7
47
|
|
|
8
48
|
**类型**:feat(架构转向:route B——插件自带 bash 工具,不再替换宿主执行器;DSH STORE 收录合规)。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"icon.svg"
|
|
53
53
|
],
|
|
54
54
|
"scripts": {
|
|
55
|
-
"test": "node --test tests/smoke.mjs",
|
|
55
|
+
"test": "node --test tests/smoke.mjs tests/private-executor.mjs",
|
|
56
|
+
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs",
|
|
56
57
|
"version": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dsh.plugin.json','utf8'));const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;m.version=v;fs.writeFileSync('dsh.plugin.json',JSON.stringify(m,null,2)+'\\n')\" && git add dsh.plugin.json"
|
|
57
58
|
},
|
|
58
59
|
"keywords": [
|
package/src/bash-tool.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* instead ships an OWN tool:
|
|
12
12
|
*
|
|
13
13
|
* - the executor is a private `GitBashSandboxExecutor` instance constructed
|
|
14
|
-
* through cordis's own plugin path in a
|
|
15
|
-
* `ctx.shell`, which stays
|
|
14
|
+
* through cordis's own plugin path in a `shell`-isolated scope (never
|
|
15
|
+
* registered as the ROOT `ctx.shell`, which stays PowerShell); it inherits
|
|
16
16
|
* the shipped bash-sandbox semantics (deadline, output capture, argv
|
|
17
17
|
* execution) and points them at Git for Windows' bash.exe;
|
|
18
18
|
* - the tool definition mirrors `@deepseek-ai/dsh-tool-bash` call-for-call
|
package/src/fastctx-bridge.js
CHANGED
|
@@ -209,6 +209,42 @@ export class McpStdio {
|
|
|
209
209
|
* the fastctx shapes: `path`, `file_path`, `files[].path`, and an absolute
|
|
210
210
|
* glob `pattern`. Pure.
|
|
211
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* Validate one pattern argument BEFORE dispatch (v0.38.5 guard, reshaped in
|
|
214
|
+
* v0.39.1 around the upstream SCHEMA — issue #21). glob's `pattern` is an
|
|
215
|
+
* untagged enum of string | string[] in tools/list (the array is the PRIMARY
|
|
216
|
+
* form — the schema's own example is an array), so a string-only check bans
|
|
217
|
+
* the main usage; and a JSON-stringified array ("[\"*.txt\"]") is matched by
|
|
218
|
+
* the upstream as ONE literal glob and silently returns nothing — the exact
|
|
219
|
+
* silent-wrong-answer this guard exists to prevent. replace's pattern is a
|
|
220
|
+
* single regex string. Pure; returns an error message or undefined.
|
|
221
|
+
* @param {string} rawName - upstream tool name ('glob' | 'replace' | ...).
|
|
222
|
+
* @param {unknown} pattern - the raw pattern argument.
|
|
223
|
+
* @returns {string|undefined}
|
|
224
|
+
*/
|
|
225
|
+
export function validateFastctxPattern(rawName, pattern) {
|
|
226
|
+
if (rawName !== 'glob' && rawName !== 'replace') return undefined
|
|
227
|
+
const describe = (v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)
|
|
228
|
+
if (rawName === 'replace') {
|
|
229
|
+
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
230
|
+
return `invalid arguments: ${gbName(rawName)} pattern must be a non-empty string (got ${describe(pattern)})`
|
|
231
|
+
}
|
|
232
|
+
return undefined
|
|
233
|
+
}
|
|
234
|
+
// A stringified array would match as one literal glob and return an empty
|
|
235
|
+
// set: name the mistake, never guess the intent (un-serializing on the
|
|
236
|
+
// model's behalf would hide the error this guard is here to surface).
|
|
237
|
+
if (typeof pattern === 'string' && pattern.length >= 2 && pattern.startsWith('[') && pattern.endsWith(']')) {
|
|
238
|
+
return `invalid arguments: ${gbName(rawName)} pattern looks like a JSON-stringified array (${String(pattern).slice(0, 60)}). Pass a real array of strings (e.g. ["*.js","*.py"]) or one plain glob string — a stringified array matches nothing.`
|
|
239
|
+
}
|
|
240
|
+
const ok = (typeof pattern === 'string' && pattern.trim() !== '')
|
|
241
|
+
|| (Array.isArray(pattern) && pattern.length > 0 && pattern.every((p) => typeof p === 'string' && p.trim() !== ''))
|
|
242
|
+
if (!ok) {
|
|
243
|
+
return `invalid arguments: ${gbName(rawName)} pattern must be a non-empty string or a non-empty array of non-empty strings (got ${describe(pattern)})`
|
|
244
|
+
}
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
212
248
|
export function translateFastctxArgs(args, msysToWin) {
|
|
213
249
|
if (!msysToWin || !args || typeof args !== 'object') return args
|
|
214
250
|
const translate = (value) => (typeof value === 'string' ? msysToWin(value) : value)
|
|
@@ -334,25 +370,28 @@ export function mountFastctx(ctx, deps) {
|
|
|
334
370
|
: rawName === 'grep'
|
|
335
371
|
? '\nIgnore rules: this tool honors .gitignore and skips .git — unlike gb_glob (bare .ignore only) and the official glob (ignores nothing).'
|
|
336
372
|
: ''
|
|
373
|
+
// Pattern-shape transparency (v0.39.1, issue #21): models keep
|
|
374
|
+
// JSON-stringifying the glob array (24 of 25 measured calls), which the
|
|
375
|
+
// upstream matches as one literal glob -> silent empty. State the
|
|
376
|
+
// accepted shapes on the tool.
|
|
377
|
+
const patternNote = rawName === 'glob'
|
|
378
|
+
? '\npattern accepts one glob string OR an array of glob strings (e.g. ["**/*.rs", "!tests/**"]). Pass a real array — never a JSON-stringified array.'
|
|
379
|
+
: ''
|
|
337
380
|
try {
|
|
338
381
|
const dispose = registry.register({
|
|
339
382
|
name: gbName(rawName),
|
|
340
|
-
description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}${ignoreNote}`,
|
|
383
|
+
description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}${ignoreNote}${patternNote}`,
|
|
341
384
|
parameters: schema,
|
|
342
385
|
output: {
|
|
343
386
|
schema: TOOL_OUTPUT_SCHEMA,
|
|
344
387
|
render: (_args, value) => renderTextContent(value),
|
|
345
388
|
},
|
|
346
389
|
async execute(args) {
|
|
347
|
-
// Fail LOUD on malformed patterns (v0.38.5
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const pattern = args.pattern
|
|
353
|
-
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
354
|
-
throw new Error(`invalid arguments: ${gbName(rawName)} pattern must be a non-empty string (got ${pattern === null ? 'null' : typeof pattern})`)
|
|
355
|
-
}
|
|
390
|
+
// Fail LOUD on malformed patterns (v0.38.5; reshaped v0.39.1,
|
|
391
|
+
// issue #21 — see validateFastctxPattern).
|
|
392
|
+
if (args !== null && typeof args === 'object' && 'pattern' in args) {
|
|
393
|
+
const invalid = validateFastctxPattern(rawName, args.pattern)
|
|
394
|
+
if (invalid) throw new Error(invalid)
|
|
356
395
|
}
|
|
357
396
|
const translated = translateFastctxArgs(args, msysToWin)
|
|
358
397
|
const result = await callWithReconnect(rawName, translated)
|
package/src/inline-tools.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* 1. `pwsh` present + `bash` absent -> register the plugin's OWN `bash`
|
|
27
27
|
* tool (src/bash-tool.js) in an agent-owned scope. It executes through a
|
|
28
28
|
* PRIVATE GitBashSandboxExecutor instance (constructed once per process
|
|
29
|
-
* in a
|
|
29
|
+
* in a shell-isolated cordis scope, never registered as the ROOT shell), and mirrors
|
|
30
30
|
* the official tool's schema, background jobs, escalation and rendering
|
|
31
31
|
* call-for-call.
|
|
32
32
|
* 2. `ops_*` tools present (dsh-ops installed) -> our FastCtx bridge stands
|
|
@@ -137,6 +137,43 @@ const HOST_MODULES = [
|
|
|
137
137
|
['llmModule', '@deepseek-ai/dsh-llm'],
|
|
138
138
|
]
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Mount the executor through the host class-plugin path. A plugin fiber owns
|
|
142
|
+
* effects, but does NOT create a service namespace: ShellExecutor's constructor
|
|
143
|
+
* still provides `shell`. Isolate ONLY that service, retaining the host's
|
|
144
|
+
* subprocess/sandbox/policy dependencies and leaving the official shell intact.
|
|
145
|
+
*
|
|
146
|
+
* The empty scope does not inject shell, so read its active provider through
|
|
147
|
+
* get(), not the property-access path that requires an inject declaration.
|
|
148
|
+
*/
|
|
149
|
+
export async function mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope, { timeoutMs = 8000 } = {}) {
|
|
150
|
+
const executorScope = createScope(ctx.isolate('shell'))
|
|
151
|
+
let timer
|
|
152
|
+
try {
|
|
153
|
+
const mountTimeout = new Promise((_, reject) => {
|
|
154
|
+
timer = setTimeout(() => reject(new Error('executor scope mount timed out (a required service may be absent in this composition)')), timeoutMs)
|
|
155
|
+
})
|
|
156
|
+
const executor = await Promise.race([
|
|
157
|
+
(async () => {
|
|
158
|
+
if (executorScope.fiber) await executorScope.fiber
|
|
159
|
+
const fiber = executorScope.ctx.plugin(GitBashSandboxExecutor)
|
|
160
|
+
await fiber
|
|
161
|
+
return executorScope.ctx.get('shell')
|
|
162
|
+
})(),
|
|
163
|
+
mountTimeout,
|
|
164
|
+
])
|
|
165
|
+
if (!executor || typeof executor.resolve !== 'function' || typeof executor.execute !== 'function') {
|
|
166
|
+
throw new Error('executor instance did not expose resolve()/execute()')
|
|
167
|
+
}
|
|
168
|
+
return { executor, executorScope }
|
|
169
|
+
} catch (error) {
|
|
170
|
+
await executorScope.dispose()
|
|
171
|
+
throw error
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timer)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
140
177
|
/**
|
|
141
178
|
* Mount the per-agent reconcile. `deps`:
|
|
142
179
|
* - tools(ctx) -> the host tool registry (or undefined)
|
|
@@ -188,20 +225,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
188
225
|
throw new Error('executor Config unavailable (schemastery unresolved; see the schemastery log lines above)')
|
|
189
226
|
}
|
|
190
227
|
if (typeof createScope !== 'function') throw new Error('no createScope seam')
|
|
191
|
-
const executorScope =
|
|
192
|
-
if (executorScope.fiber) { try { await executorScope.fiber } catch { /* disposed */ } }
|
|
193
|
-
const mountTimeout = new Promise((_, reject) => {
|
|
194
|
-
const timer = setTimeout(() => reject(new Error('executor scope mount timed out (a required service may be absent in this composition)')), 8000)
|
|
195
|
-
timer.unref?.()
|
|
196
|
-
})
|
|
197
|
-
const fiber = executorScope.ctx.plugin(GitBashSandboxExecutor)
|
|
198
|
-
const executor = await Promise.race([
|
|
199
|
-
(async () => { await fiber; return executorScope.ctx.shell })(),
|
|
200
|
-
mountTimeout,
|
|
201
|
-
])
|
|
202
|
-
if (!executor || typeof executor.resolve !== 'function' || typeof executor.execute !== 'function') {
|
|
203
|
-
throw new Error('executor instance did not expose resolve()/execute()')
|
|
204
|
-
}
|
|
228
|
+
const { executor, executorScope } = await mountPrivateExecutor(ctx, GitBashSandboxExecutor, createScope)
|
|
205
229
|
const hostModules = {}
|
|
206
230
|
for (const [key, name] of HOST_MODULES) {
|
|
207
231
|
try {
|
|
@@ -226,7 +250,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
226
250
|
// Same asynchronous-fiber shape the official module needed: wait for
|
|
227
251
|
// the apply callback to capture scope.ctx before touching its tools.
|
|
228
252
|
if (scope.fiber) { try { await scope.fiber } catch { /* disposed */ } }
|
|
229
|
-
const registry = scope.ctx?.tools
|
|
253
|
+
const registry = scope.ctx?.get('tools')
|
|
230
254
|
if (!registry || typeof scope.ctx.plugin !== 'function') {
|
|
231
255
|
void scope.dispose().catch(() => {})
|
|
232
256
|
return
|
package/src/shell.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-gitbash-shell/shell — the Git Bash executor INSTANCE (route B, v0.39.0).
|
|
3
3
|
*
|
|
4
|
-
* Since route B this class is NOT mounted as the
|
|
4
|
+
* Since route B this class is NOT mounted as the ROOT `ctx.shell` service:
|
|
5
5
|
* the official PowerShell executor keeps that slot untouched (the DSH-Store
|
|
6
6
|
* catalog gate forbids bundle patches that disable official components, and
|
|
7
7
|
* replacing the shell executor IS overriding one). Instead the plugin
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
* executor did.
|
|
37
37
|
*
|
|
38
38
|
* Constructed as a cordis class plugin (static inject/Config honoured) —
|
|
39
|
-
* inside
|
|
40
|
-
*
|
|
39
|
+
* inside an explicitly `ctx.isolate('shell')`-isolated scope, so the base
|
|
40
|
+
* class's service registration never touches the root `ctx.shell` slot.
|
|
41
|
+
* A plain child plugin fiber is NOT a service isolation boundary.
|
|
41
42
|
*/
|
|
42
43
|
|
|
43
44
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|