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 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.0",
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.0",
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 PRIVATE scope (never registered as
15
- * `ctx.shell`, which stays the official PowerShell executor); it inherits
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
@@ -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, the field smoke
348
- // report): the upstream server silently treats a non-string
349
- // pattern as "everything", which turns a typo into a full-tree
350
- // result set — a silent wrong answer is worse than an error.
351
- if ((rawName === 'glob' || rawName === 'replace') && args !== null && typeof args === 'object' && 'pattern' in args) {
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)
@@ -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 private cordis scope, never registered as a service), and mirrors
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 = createScope(ctx)
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 host `ctx.shell` service:
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 the PRIVATE scope only, so the base class's service registration
40
- * lands there and never touches the root `ctx.shell` slot.
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'