dsh-gitbash-shell 0.35.1 → 0.35.3
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 +23 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/fastctx-bridge.js +9 -0
- package/src/index.js +2 -1
- package/src/inline-tools.js +89 -18
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.35.3 — 2026-10-12
|
|
7
|
+
|
|
8
|
+
**类型**:fix(issue [#18](https://github.com/KannaKuron/dsh-gitbash-shell/issues/18) 三个独立根因;Windows 真机 v0.35.0 四面全灭)。
|
|
9
|
+
|
|
10
|
+
外部用户(zjh1159086960-crypto / Lntano)的高质量报告:v0.35.0 四个新面(行内补位 bash、假 pwsh 遮蔽、`gitbash_pwsh`、`gb_*`)在 Windows 真机**一个都没生效**,且定位到行。三个根因相互独立,v0.35.2 的挂载时扫描只是让「断得更早的 wiring」多了一次运行机会——**根因仍在,四面依然全灭**。本版逐一修复,修法全部对照 dsh-ops 已在真机验证的实现:
|
|
11
|
+
|
|
12
|
+
- **根因一:`resolveHostPwsh7()` 零参调用 → 整个行内补位 wiring 流产**(主凶)。`exists` 是必填注入项,win32 下对象字面量求值即抛 `exists is not a function`,被 `mountInlineTools` 外层 catch 吞成一行日志——bash 补位、假 pwsh 遮蔽、`gitbash_pwsh` 三个面同死。修复:调用点注入 `{ exists: existsSync }`(`node:fs`,候选矩阵与 PATH 兜底逻辑本身无恙,报告者已验证)。
|
|
13
|
+
- **根因二:工具定义缺 host 必填的 `output` 契约 → `gitbash_pwsh` 与 `gb_*` 注册全被拒**。host `ToolRuntime.register` 硬校验 `output { schema, render }`(官方工具全部经 `defineTool` 包装补出)。修复:新增冻结的 `TOOL_OUTPUT_SCHEMA` + `renderTextContent`(dsh-ops `lib/tools.js` 的 OUTPUT_SCHEMA 同款形状,两个工具家族共享),`gitbashPwshDefinition` 与 `gb_*` 注册全部补上。
|
|
14
|
+
- **根因三:`restrict()` 从根 ctx 调用必然抛错 → 假 pwsh 的 deny 从未建立**。host 硬校验「requires a scoped context」,根 ctx 无 scope 键,异常被 catch 吞成 `fake-pwsh deny failed` 日志。修复:改走 **`agent.ctx.tools.restrict({ deny: ['pwsh'] })`**(dsh-ops `lib/index.js` `installHostShellDenial` 的同款调用;host 文档原文「Hiding tools from one agent is ctx.tools.restrict() on that agent's context」)。
|
|
15
|
+
- **冒烟**:116 项(115 过 + 1 跳)。新增两个测试:「the three issue-#18 root causes stay fixed」(三个根因的源码防回归锁,含 `doesNotMatch` 禁止零参调用与根 ctx restrict 回潮)与「the tool definitions speak the host output contract」(两家族共享契约 + render 行为:多块拼接、空 content 渲染单个空文本块)。
|
|
16
|
+
- **致谢**:三个根因全部由报告者先行定位,本版修复与其复现脚本逐一对齐;真机验证(四面生效)待报告者与用户复验。
|
|
17
|
+
|
|
18
|
+
## v0.35.2 — 2026-10-12
|
|
19
|
+
|
|
20
|
+
**类型**:fix(Windows 真机报告;行内补位的时序洞 + 调用层兜底)。
|
|
21
|
+
|
|
22
|
+
- **修复:已存在的 agent 拿不到补位(用户 Windows 真机报告)**。症状:AI 调 `pwsh` 工具收到 `/usr/bin/bash: line 1: Select-String: command not found`——假 pwsh 的执行器在本机就是 `ctx.shell`(Git Bash),PowerShell 命令进了 bash。根因:补位判定此前**只挂在 `agent/created`** 上,本插件行**激活晚于 agent 存在**时(插件热重载、宿主先恢复会话后本 bundle 行才挂完),全部已存在的 agent 保持假 pwsh 且没有 bash,且永不再触发判定。修复为 **dsh-ops 验证过的双重独立层**(`installHostShellDenial` 同款形态):
|
|
23
|
+
- **层 1 — 挂载时补扫**:`mountInlineTools` 新增 `agents(ctx)` 依赖(index.js 传入 `ctx.get('agents')`),挂载那一刻对 `agents.list()` 里的每个**已存活 agent** 跑一遍 `reconcile`——与 created 事件同一条路径,250ms×5 有界重试等同样生效;
|
|
24
|
+
- **层 2 — 调用层守卫**:新增 `tools/pre-execute` 监听,凡 `pwsh` 调用到达分发层一律 deny,理由句直接指路替换品(bash 工具 / `gitbash_pwsh`)。两层层层独立:即便 per-agent 的 `restrict({ deny })` 输了竞态,谎工具也到不了执行器。
|
|
25
|
+
- **顺手修复:重名声明 SyntaxError**。v0.35.2 开发中引入的 `agents` 依赖与 `mountInlineTools` 内部的 agent 状态 Map 同名(`const` 重复声明,模块整体加载失败)。内部 Map 改名 `tracked`;冒烟加防回归断言(源码不得再现 `const agents = new Map()`)。
|
|
26
|
+
- **冒烟**:新增「inline tools: the mount-time sweep and the call-layer guard exist (v0.35.2)」;「…wires the patch…」补 `agents` 依赖接线断言。合计 114 项(113 过 + 1 跳)。
|
|
27
|
+
- **Windows 真机复验清单(升级后)**:①官方模式新会话有 bash 工具;②假 pwsh 调用被拒(deny 理由指路);③`gitbash_pwsh` 跑真 pwsh 7;④`gb_grep` 出参 /c/ 方言。v0.35.0 的真机验证项(见该条目)顺延至此版一起验。
|
|
28
|
+
|
|
6
29
|
## v0.35.1 — 2026-10-12
|
|
7
30
|
|
|
8
31
|
**类型**:fix(dsh-ptc-cordis-preset 联动显示;真机反馈)。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:用 Git for Windows bash 替换 PowerShell 执行器;官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0,默认),四个 *·Git Bash 变体 preset 可开关;附带 gitbash_pwsh(系统 PowerShell 7)与 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.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"description": "Windows 上让 agent 的每条 shell 命令改经 Git for Windows bash 执行;物化四个内置预设的 Git Bash 变体,统一 MSYS 盘根路径方言,侧栏终端接管可开关。· Run every agent shell command through Git for Windows bash on Windows; materialize Git Bash variants of the built-in presets, unify the MSYS drive-root path dialect, and adopt the sidebar terminal behind a switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/fastctx-bridge.js
CHANGED
|
@@ -42,6 +42,11 @@ export const FASTCTX_FILE_TOOLS = Object.freeze(['inspect_local_file', 'grep', '
|
|
|
42
42
|
export const GB_PREFIX = 'gb_'
|
|
43
43
|
export const gbName = (raw) => GB_PREFIX + raw
|
|
44
44
|
|
|
45
|
+
// The HOST registry rejects any tool without an `output` contract (issue #18,
|
|
46
|
+
// root cause 2) — the frozen schema + text renderer the gitbash_pwsh
|
|
47
|
+
// definition ships, reused so both families speak one tool-result contract.
|
|
48
|
+
import { TOOL_OUTPUT_SCHEMA, renderTextContent } from './inline-tools.js'
|
|
49
|
+
|
|
45
50
|
/** Payload packages probed, in order: upstream first, the pinned fork second. */
|
|
46
51
|
export const PAYLOAD_PACKAGES = Object.freeze([
|
|
47
52
|
{ pkg: '@fastctx/win32-x64', exe: 'bin/fastctx.exe' },
|
|
@@ -323,6 +328,10 @@ export function mountFastctx(ctx, deps) {
|
|
|
323
328
|
name: gbName(rawName),
|
|
324
329
|
description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}`,
|
|
325
330
|
parameters: schema,
|
|
331
|
+
output: {
|
|
332
|
+
schema: TOOL_OUTPUT_SCHEMA,
|
|
333
|
+
render: (_args, value) => renderTextContent(value),
|
|
334
|
+
},
|
|
326
335
|
async execute(args) {
|
|
327
336
|
const translated = translateFastctxArgs(args, msysToWin)
|
|
328
337
|
const result = await callWithReconnect(rawName, translated)
|
package/src/index.js
CHANGED
|
@@ -2608,6 +2608,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2608
2608
|
try {
|
|
2609
2609
|
mountInlineTools(ctx, {
|
|
2610
2610
|
tools: (c) => { try { return c.get('tools') } catch { return undefined } },
|
|
2611
|
+
agents: (c) => { try { return c.get('agents') } catch { return undefined } },
|
|
2611
2612
|
createScope: (agentCtx) => {
|
|
2612
2613
|
let childCtx
|
|
2613
2614
|
const fiber = agentCtx.plugin((c) => { childCtx = c })
|
|
@@ -2624,7 +2625,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2624
2625
|
if (!subprocess || typeof subprocess.spawn !== 'function') throw new Error('the host subprocess service is not available on this deployment')
|
|
2625
2626
|
return subprocess.spawn(spec)
|
|
2626
2627
|
},
|
|
2627
|
-
pwshPath: resolveHostPwsh7(),
|
|
2628
|
+
pwshPath: resolveHostPwsh7({ exists: existsSync }),
|
|
2628
2629
|
pwshEnabled: () => liveSettings.pwshTool(),
|
|
2629
2630
|
msysToWin: (p) => translateMsysPath(p, buildTranslateEnv(readConfiguredBashPath())),
|
|
2630
2631
|
echo: (text) => (liveSettings.dialect().posixPaths === true ? windowsToMsys(text) : text),
|
package/src/inline-tools.js
CHANGED
|
@@ -37,6 +37,44 @@
|
|
|
37
37
|
* loads but mounts nothing.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The tool-result contract the HOST registry requires from every registered
|
|
42
|
+
* tool (issue #18, root cause 2): `register()` throws
|
|
43
|
+
* `must declare output { schema, render, presentationMeta? }` unless the
|
|
44
|
+
* definition carries an `output` whose `schema` passes
|
|
45
|
+
* `assertSupportedJsonSchema` and whose `render` is a function. Both of this
|
|
46
|
+
* plugin's tool families return `{ content: [{ type: 'text', text }] }` from
|
|
47
|
+
* `execute`, so the schema and the text renderer below are shared verbatim
|
|
48
|
+
* with the FastCtx bridge — the same frozen shape dsh-ops ships for its own
|
|
49
|
+
* MCP-backed tools (proven on real Windows hosts).
|
|
50
|
+
*/
|
|
51
|
+
export const TOOL_OUTPUT_SCHEMA = Object.freeze({
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
content: { type: 'array', items: {} },
|
|
55
|
+
structuredContent: {},
|
|
56
|
+
},
|
|
57
|
+
required: ['content'],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render the `{ content: [...] }` execute value into text blocks. Non-text
|
|
63
|
+
* blocks and non-string text fields are skipped; empty content renders as one
|
|
64
|
+
* empty text block rather than an empty array, so the card never loses its
|
|
65
|
+
* placeholder.
|
|
66
|
+
* @param {unknown} value - the execute return value.
|
|
67
|
+
* @returns {Array<{ type: string, text: string }>} content blocks for the host renderer.
|
|
68
|
+
*/
|
|
69
|
+
export function renderTextContent(value) {
|
|
70
|
+
const blocks = Array.isArray(value?.content) ? value.content : []
|
|
71
|
+
const text = blocks
|
|
72
|
+
.map((block) => (typeof block?.text === 'string' ? block.text : ''))
|
|
73
|
+
.filter((line) => line !== '')
|
|
74
|
+
.join('\n')
|
|
75
|
+
return [{ type: 'text', text }]
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
/** Answer for one observed tool face. Pure. */
|
|
41
79
|
export function inlineNeeds(names) {
|
|
42
80
|
const set = new Set(Array.isArray(names) ? names : [])
|
|
@@ -223,6 +261,7 @@ export async function activateOfficialBash(agent, toolBashHref, createScopeFn, l
|
|
|
223
261
|
/**
|
|
224
262
|
* Mount the per-agent reconcile. `deps`:
|
|
225
263
|
* - tools(ctx) -> the host tool registry (or undefined)
|
|
264
|
+
* - agents(ctx) -> the host agents service (or undefined)
|
|
226
265
|
* - createScope(fn) -> cordis createScope
|
|
227
266
|
* - allowed(session) -> danger-full-access verdict for the pwsh channel
|
|
228
267
|
* - echo(text) -> result dialect echo (Windows -> MSYS), or undefined
|
|
@@ -231,15 +270,20 @@ export async function activateOfficialBash(agent, toolBashHref, createScopeFn, l
|
|
|
231
270
|
* - pwshEnabled() -> the row switch for publishing the pwsh tool
|
|
232
271
|
* - log(message) -> the plugin's tagged logger
|
|
233
272
|
*
|
|
234
|
-
* Registration decisions re-run on EVERY agent creation
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
273
|
+
* Registration decisions re-run on EVERY agent creation AND once at mount
|
|
274
|
+
* time for every ALREADY-LIVE agent (the dsh-ops-proven shape,
|
|
275
|
+
* `installHostShellDenial`): this row can activate long after agents exist —
|
|
276
|
+
* a plugin hot-reload, or a host whose sessions resumed before this bundle
|
|
277
|
+
* row finished mounting — and an agent/created-only listener leaves every
|
|
278
|
+
* such agent with the fake pwsh and no bash, which is exactly the Windows
|
|
279
|
+
* field report behind this block. A second, INDEPENDENT layer guards the
|
|
280
|
+
* call itself through `tools/pre-execute`: visibility restriction can lose a
|
|
281
|
+
* race, and a lying pwsh that reaches its executor still runs Git Bash.
|
|
238
282
|
*/
|
|
239
283
|
export function mountInlineTools(ctx, deps) {
|
|
240
|
-
const { tools, createScope, allowed, spawn, pwshPath, echo, log } = deps
|
|
241
|
-
/** agent -> { timer, scope, denyPwsh } */
|
|
242
|
-
const
|
|
284
|
+
const { tools, agents, createScope, allowed, spawn, pwshPath, echo, log } = deps
|
|
285
|
+
/** agent -> { timer, scope, denyPwsh }; named `tracked` because `agents` above is the host service */
|
|
286
|
+
const tracked = new Map()
|
|
243
287
|
let pwshPublished = false
|
|
244
288
|
let pwshDisposer = undefined
|
|
245
289
|
let toolBashHrefPromise
|
|
@@ -287,24 +331,27 @@ export function mountInlineTools(ctx, deps) {
|
|
|
287
331
|
if (attempt < 5 && !names.includes('bash') && !names.includes('pwsh')) {
|
|
288
332
|
const timer = setTimeout(() => { try { reconcile(agent, { attempt: attempt + 1 }) } catch { /* never crash on a timer */ } }, 250)
|
|
289
333
|
timer.unref?.()
|
|
290
|
-
const state =
|
|
334
|
+
const state = tracked.get(agent)
|
|
291
335
|
if (state) state.timer = timer
|
|
292
|
-
else
|
|
336
|
+
else tracked.set(agent, { timer, scope: undefined, denyPwsh: undefined })
|
|
293
337
|
return
|
|
294
338
|
}
|
|
295
339
|
const needs = inlineNeeds(names)
|
|
296
|
-
let state =
|
|
340
|
+
let state = tracked.get(agent)
|
|
297
341
|
if (state?.timer) { clearTimeout(state.timer); state.timer = undefined }
|
|
298
342
|
if (!state) {
|
|
299
343
|
state = { timer: undefined, scope: undefined, denyPwsh: undefined }
|
|
300
|
-
|
|
344
|
+
tracked.set(agent, state)
|
|
301
345
|
}
|
|
302
346
|
if (needs.denyPwsh && !state.denyPwsh) {
|
|
303
347
|
// Deny the fake pwsh first: restrict is per-agent and idempotent, and
|
|
304
348
|
// the lying tool must never be callable, not even for the milliseconds
|
|
305
|
-
// before the bash tool lands.
|
|
349
|
+
// before the bash tool lands. The call MUST go through the AGENT's
|
|
350
|
+
// scoped context (`agent.ctx.tools`, the dsh-ops-proven shape): on the
|
|
351
|
+
// ROOT context the host throws "requires a scoped context" and the
|
|
352
|
+
// deny silently never exists (issue #18, root cause 3).
|
|
306
353
|
try {
|
|
307
|
-
const deny =
|
|
354
|
+
const deny = agent.ctx?.tools?.restrict?.({ deny: ['pwsh'] })
|
|
308
355
|
if (typeof deny === 'function') state.denyPwsh = deny
|
|
309
356
|
} catch (error) {
|
|
310
357
|
log(`fake-pwsh deny failed: ${error?.message ?? error}`)
|
|
@@ -318,7 +365,7 @@ export function mountInlineTools(ctx, deps) {
|
|
|
318
365
|
return
|
|
319
366
|
}
|
|
320
367
|
const scope = await activateOfficialBash(agent, href, createScope, log)
|
|
321
|
-
if (scope &&
|
|
368
|
+
if (scope && tracked.get(agent) === state) {
|
|
322
369
|
state.scope = scope
|
|
323
370
|
log('official bash tool activated for this agent (Git Bash host; fake pwsh denied)')
|
|
324
371
|
}
|
|
@@ -334,9 +381,9 @@ export function mountInlineTools(ctx, deps) {
|
|
|
334
381
|
}
|
|
335
382
|
|
|
336
383
|
const teardown = (agent) => {
|
|
337
|
-
const state =
|
|
384
|
+
const state = tracked.get(agent)
|
|
338
385
|
if (!state) return
|
|
339
|
-
|
|
386
|
+
tracked.delete(agent)
|
|
340
387
|
if (state.timer) clearTimeout(state.timer)
|
|
341
388
|
if (state.denyPwsh) { try { state.denyPwsh() } catch { /* already lifted */ } }
|
|
342
389
|
if (state.scope) { void state.scope.dispose().catch(() => {}) }
|
|
@@ -345,14 +392,34 @@ export function mountInlineTools(ctx, deps) {
|
|
|
345
392
|
try {
|
|
346
393
|
ctx.on('agent/created', ({ agent } = {}) => { try { reconcile(agent) } catch { /* never break agent creation */ } })
|
|
347
394
|
ctx.on('agent/disposed', ({ agent } = {}) => { try { teardown(agent) } catch { /* never break disposal */ } })
|
|
395
|
+
// MOUNT-TIME SWEEP (v0.35.2, the Windows field report): agents that
|
|
396
|
+
// already exist when this row mounts never fire agent/created — a plugin
|
|
397
|
+
// hot-reload or a host that resumed sessions before this bundle row
|
|
398
|
+
// finished mounting would otherwise keep the fake pwsh and no bash on
|
|
399
|
+
// every live agent. Same shape the proven dsh-ops denial uses.
|
|
400
|
+
for (const agent of agents?.(ctx)?.list?.() ?? []) {
|
|
401
|
+
try { reconcile(agent) } catch { /* one broken agent never blocks the rest */ }
|
|
402
|
+
}
|
|
403
|
+
// CALL-LAYER GUARD (v0.35.2, second independent layer): even when the
|
|
404
|
+
// per-agent visibility restriction loses a race, a `pwsh` call that
|
|
405
|
+
// reaches its executor runs Git Bash (ctx.shell IS Git Bash on this
|
|
406
|
+
// host). Deny at dispatch and name the replacements.
|
|
407
|
+
ctx.on('tools/pre-execute', (exec, next) => {
|
|
408
|
+
try {
|
|
409
|
+
if (exec?.name === 'pwsh') {
|
|
410
|
+
return { kind: 'deny', reason: 'the host pwsh tool would run Git Bash on this host (its executor was replaced). Use the bash tool for commands, or gitbash_pwsh for Windows-native work (registry, services, WMI).' }
|
|
411
|
+
}
|
|
412
|
+
} catch { /* never block the waterfall */ }
|
|
413
|
+
return next()
|
|
414
|
+
})
|
|
348
415
|
} catch (error) {
|
|
349
416
|
log(`agent lifecycle wiring failed: ${error?.message ?? error}`)
|
|
350
417
|
return
|
|
351
418
|
}
|
|
352
419
|
|
|
353
420
|
ctx.effect(() => () => {
|
|
354
|
-
for (const agent of [...
|
|
355
|
-
|
|
421
|
+
for (const agent of [...tracked.keys()]) teardown(agent)
|
|
422
|
+
tracked.clear()
|
|
356
423
|
unpublishPwsh()
|
|
357
424
|
}, 'dsh-gitbash-shell: inline tool patching')
|
|
358
425
|
}
|
|
@@ -387,6 +454,10 @@ export function gitbashPwshDefinition({ pwsh, spawn, allowed, msysToWin, echo })
|
|
|
387
454
|
required: ['command'],
|
|
388
455
|
additionalProperties: false,
|
|
389
456
|
},
|
|
457
|
+
output: {
|
|
458
|
+
schema: TOOL_OUTPUT_SCHEMA,
|
|
459
|
+
render: (_args, value) => renderTextContent(value),
|
|
460
|
+
},
|
|
390
461
|
async execute(args, exec) {
|
|
391
462
|
const invalid = validatePwshArgs(args)
|
|
392
463
|
if (invalid) throw new Error(invalid)
|