Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。

## v0.48.0 — 2026-11-02

### fix:工具面开关改为「重启生效」——不再在存活会话上重建工具面

- **背景(真机事故)**:在设置卡把 `pwshTool` 关掉的同一条消息之后,正在运行的会话工具面被清空——
`request/header.tools` 从 59 个掉到 31 个,丢掉的 29 个正是官方 preset 的工具
(`read`/`write`/`edit`/`glob`/`grep`/`bash`/`gitbash_native`/`pwsh`/`web_search`/`web_fetch`/
`todo_write`/`exit_plan_mode`/`schedule_*`/`skill`/`present` 等),只剩 profile 级插件工具;
重启 DSH 后恢复。环境:dsh 0.2.1-rc.5 + 本插件 0.47.2,win32 真机。
- **插件侧成因**:`pwshTool`/`experimentalNative` 是 volatile 字段,宿主把编辑实时交给运行中的行;
本插件在 `loader/volatile-update` 上对**每个存活 agent** 重跑 `reconcile()`,而 `reconcile()`
会先解除自己的 pwsh mask 再读工具面——这次读取与宿主重组的时序不保证稳定,既可能把「face 不含 pwsh」
误判成「preset mask 接管」而撤掉自己注册的 bash,也让一次纯展示开关的改动触发工具面重建。
- **改动**:
- 每个 agent 在**首次观测**时快照 `{ pwshVisible, experimental }`,此后一律按快照判定,不再读 live 开关;
- `loader/volatile-update` 命中这两个开关时**只写一行日志**
(`tool-face switch changed (pwshTool): running agents keep their tool face; ...`),
不再重建任何存活 agent 的工具面;
- `tools/pre-execute` 的 pwsh 拒绝判定跟随该 agent 的快照(保留 `deps.pwshEnabled?.() !== true` 字面量);
`ensureNativeSlot` / `nativeWanted` 也改为按快照的 `experimental` 创建/保留 native 槽,不再被 live 开关中途撤掉;
- 设置卡只给 `pwshTool` / `experimentalNative` 两个开关加「工具面可见性需重启」的中英提示
(`variantPresets` / `fastctxTools` 维持原语义);`experimentalNative` 的原生工具执行门禁仍按当前开关立即生效。
- **行为契约变化**:这两个开关的**工具面可见性/注册**不再对已存在会话生效,新会话或重启后应用新值;
执行门禁仍可立即生效。README 原写「切换 `pwshTool` 会改 bash description、一次性作废缓存」,
现改为「工具面保存后需重启」,不再发生中途的工具面重建。
- **测试(346 项:345 pass + 1 平台 skip)**:`runtime-regressions` 的「活体跟随两次翻转」改为「冻结 + 只记日志」,
并新增「翻转只对 NEW agent 生效」用例(fixture 的 mask 改为 per-agent);macOS 全量通过,PR 作者在
Windows 0.2.1-rc.5 真机复验原补丁。本次合入后的补丁未再上 Windows,Windows 侧仍需按 #38 的
`request/header` 方法复验一次。
- **未覆盖**:没能定位宿主侧「preset 工具整批消失」的完整链路(丢掉的 29 个工具超出本插件可影响的
范围);本改动消除插件侧触发点并让 UI 说明真实生效时机,宿主侧重组时序仍需宿主日志才能确认。

## v0.47.2 — 2026-11-01

### fix:兼容下限回落到 0.1.7-rc,恢复旧宿主的执行器形状与工具面回退
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ POSIX 宿主上完整保留;
原生操作(cmdlet/注册表/服务/WMI)。默认关闭(`pwshTool` 开关):该工具对本插件
会话隐藏,主机保持 bash 优先;开启后它直接可见可用——无需安装任何额外组件。
旧 PowerShell 7 旁路工具(v0.37–v0.38.5 的独立工具)已随 v0.39.0 新架构退役;
命令仍然首选 bash 工具。切换 `pwshTool` 会同时改 bash 工具的 description(路由句随开关
出现/消失),下一次请求会作废一次前缀缓存;工具名与 parameters 不变。
命令仍然首选 bash 工具。`pwshTool` 与 `experimentalNative` 的**工具面可见性**只影响新会话或重启后的
会话:运行中的会话会保持它创建时的工具面,保存开关只在宿主日志留一行提示,不会中途重建。
实验原生工具的执行门禁仍按当前开关立即生效。
- **`gb_*` FastCtx 文件工具(默认关,v0.39.4 起弃用)**:`gb_inspect_local_file`/`gb_grep`/
`gb_glob`/`gb_replace`(Rust 实现:批量范围读取带 token 预算、grep 截断
标注、编码探测)。它们是官方 read/grep/glob/edit 的**补充**——官方工具接
Expand Down
8 changes: 5 additions & 3 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ The old "replace the executor" three-step patch became runtime work:
(`src/inline-tools.js`) registers the own bash tool per agent whenever the
tool face says "pwsh yes / bash no"; the official `pwsh` tool (REAL
PowerShell) stays hidden behind the `pwshTool` switch (off by default).
Toggling `pwshTool` also changes the bash tool's description (the routing
sentence appears/disappears), invalidating one prompt-prefix cache entry on
the next request; the tool name and parameters stay unchanged. The `gb_*`
Toggling `pwshTool` or `experimentalNative` changes the tool face (visibility/
registration) for NEW sessions or after a DSH restart only: a running session
keeps the face it was created with, and saving just writes one host-log line
instead of rebuilding that face. The native tools' execution gate still
follows the current switch immediately. The `gb_*`
FastCtx tools (`src/fastctx-bridge.js`) publish alongside — **retired to
an explicit opt-in from v0.39.4** (dsh-ops 0.3.0 replaced FastCtx with
native `ops_*` file tools; when dsh-ops is present the bridge stands down
Expand Down
2 changes: 1 addition & 1 deletion dsh.plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "dsh-external/dsh-gitbash-shell",
"version": "0.47.2",
"version": "0.48.0",
"main": "./src/index.js",
"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)。",
"engines": {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsh-gitbash-shell",
"version": "0.47.2",
"version": "0.48.0",
"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.",
"type": "module",
"repository": {
Expand Down
8 changes: 4 additions & 4 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ window.__ModuleLoader__.load({
"variant.label": "四个「·Git Bash」变体模式",
"variant.hint": "默认关闭:官方四个模式经行内工具补位直接获得 Git Bash,模式列表保持官方原样。开启后注册四个变体(standard/minimal/ptc/cordis · Git Bash)并停用官方模式的工具补位——两者互斥。钉在变体 id 上的旧会话在关闭升级后需重新选择模式;变更对新会话生效。",
"experimental.label": "实验原生执行 / fail-closed",
"experimental.hint": "默认开启:Windows 受限或未确定访问模式拒绝 Git Bash,并提供 gitbash_native / gitbash_to_native_path。修改立即作用于现有会话的后续调用。关闭会隐藏并禁用两个工具,恢复明确标注 UNCONFINED 的旧版 Bash:不执行 shell 文件写入限制。如保留拒绝机制,请由用户在会话访问模式控件中选择 danger-full-access。",
"experimental.hint": "默认开启:Windows 受限或未确定访问模式拒绝 Git Bash,并提供 gitbash_native / gitbash_to_native_path。修改立即作用于现有会话的后续调用。关闭会隐藏并禁用两个工具,恢复明确标注 UNCONFINED 的旧版 Bash:不执行 shell 文件写入限制。如保留拒绝机制,请由用户在会话访问模式控件中选择 danger-full-access。 ⚠ 工具面可见性:保存后需重启 DSH 才会在运行中会话更新;原生工具的执行门禁仍按当前开关立即生效。",
"pwsh.label": "pwsh 工具(官方 PowerShell)",
"pwsh.hint": "默认关闭:开启后宿主官方的 pwsh 工具对会话可见——它是真正的 PowerShell(官方解析器自动优先 PowerShell 7),可直接用于 Windows 原生操作(注册表/服务/WMI)。关闭(默认)时该工具被隐藏,主机保持 bash 优先。无需安装任何额外组件;旧 PowerShell 7 旁路工具已随新架构退役。命令仍然首选 bash 工具。",
"pwsh.hint": "默认关闭:开启后宿主官方的 pwsh 工具对会话可见——它是真正的 PowerShell(官方解析器自动优先 PowerShell 7),可直接用于 Windows 原生操作(注册表/服务/WMI)。关闭(默认)时该工具被隐藏,主机保持 bash 优先。无需安装任何额外组件;旧 PowerShell 7 旁路工具已随新架构退役。命令仍然首选 bash 工具。 ⚠ 工具面可见性:保存后需重启 DSH 才会在运行中会话更新;不会中途丢工具。",
"fctx.label": "FastCtx 文件工具(gb_*)",
"fctx.hint": "默认关闭(v0.39.4 起弃用,可显式开启):发布 gb_inspect_local_file / gb_grep / gb_glob / gb_replace(Rust 实现:批量读取带 token 预算、编码探测)。它们是官方 read/grep/glob/edit 的补充——官方工具接文件沙箱,仍是默认推荐;检测到 dsh-ops(ops_* 工具)时本组自动让位。",
};
Expand Down Expand Up @@ -235,9 +235,9 @@ window.__ModuleLoader__.load({
"variant.label": "The four \"· Git Bash\" variant presets",
"variant.hint": "Off by default: the four OFFICIAL presets get Git Bash through the inline tool patch and the roster stays official. On: register the four variants (standard/minimal/ptc/cordis · Git Bash) and stop patching the official presets — the two are exclusive. A session pinned to a variant id must re-select a preset after upgrading with the switch off. Applies to new sessions.",
"experimental.label": "Experimental native execution / fail-closed",
"experimental.hint": "Default ON: refuse Windows Git Bash in restricted or unresolved access modes and offer gitbash_native / gitbash_to_native_path. Changes apply to subsequent calls in existing sessions. OFF hides and disables both tools and restores legacy explicitly UNCONFINED Bash; shell file-write restrictions are NOT enforced. To keep refusal enabled, select danger-full-access in the session access-mode control yourself.",
"experimental.hint": "Default ON: refuse Windows Git Bash in restricted or unresolved access modes and offer gitbash_native / gitbash_to_native_path. Changes apply to subsequent calls in existing sessions. OFF hides and disables both tools and restores legacy explicitly UNCONFINED Bash; shell file-write restrictions are NOT enforced. To keep refusal enabled, select danger-full-access in the session access-mode control yourself. ⚠ Tool-face visibility: a saved change reaches a running session only after a DSH restart; the native tools' execution gate still follows the current switch immediately.",
"pwsh.label": "pwsh tool (official PowerShell)",
"pwsh.hint": "Off by default: when on, the host's official pwsh tool becomes visible to sessions — REAL PowerShell (the official resolver prefers PowerShell 7 automatically) for Windows-native work (registry/services/WMI). Off (the default) hides it and keeps the host bash-first. Nothing extra to install; the retired PowerShell 7 side-channel tool is gone. Commands still prefer the bash tool.",
"pwsh.hint": "Off by default: when on, the host's official pwsh tool becomes visible to sessions — REAL PowerShell (the official resolver prefers PowerShell 7 automatically) for Windows-native work (registry/services/WMI). Off (the default) hides it and keeps the host bash-first. Nothing extra to install; the retired PowerShell 7 side-channel tool is gone. Commands still prefer the bash tool. ⚠ Tool-face visibility: a saved change reaches a running session only after a DSH restart; tools are never dropped mid-session.",
"fctx.label": "FastCtx file tools (gb_*)",
"fctx.hint": "Off by default since v0.39.4 (deprecated; re-enable explicitly): publishes gb_inspect_local_file / gb_grep / gb_glob / gb_replace (Rust: batched reads with a token budget, encoding detection). ADDITIONS to the official read/grep/glob/edit — the official tools keep the file sandbox and stay the default. Stands down automatically when dsh-ops is installed.",
};
Expand Down
44 changes: 33 additions & 11 deletions src/inline-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ export function mountInlineTools(ctx, deps) {
const controller = new AbortController()
let closed = false
const isLive = (agent, state) => !closed && tracked.get(agent) === state && !state.controller.signal.aborted
const newState = () => ({ timer: undefined, scope: undefined, denyPwsh: undefined, denyOpsShell: undefined, pending: undefined, native: undefined, nativeFailed: false, controller: new AbortController() })
const newState = () => ({ timer: undefined, scope: undefined, denyPwsh: undefined, denyOpsShell: undefined, pending: undefined, native: undefined, nativeFailed: false, controller: new AbortController(), toolFace: undefined })
// toolFace freezes the switch values this agent was created under, so a live
// settings edit cannot reshape a running face.
const experimentalEnabled = () => deps.experimentalEnabled?.() !== false
// Loader imports cannot be cancelled, but no continuation may outlive its
// owner. Join our cancelled continuations without waiting on a stuck loader.
Expand Down Expand Up @@ -496,7 +498,7 @@ export function mountInlineTools(ctx, deps) {
})

const ensureNativeSlot = (agent, state) => {
if (state.native || state.nativeFailed || !deps.nativeContract || !experimentalEnabled()) return
if (state.native || state.nativeFailed || !deps.nativeContract || state.toolFace?.experimental === false) return
let jobs
try { jobs = agent.ctx?.get?.('jobs') } catch { jobs = undefined }
if (!jobs) return
Expand Down Expand Up @@ -612,15 +614,24 @@ export function mountInlineTools(ctx, deps) {
else tracked.set(agent, { ...newState(), timer })
return
}
const pwshVisible = deps.pwshEnabled?.() === true
const needs = inlineNeeds(names, { pwshVisible })
if (needs.opsPresent) opsSeen = true
let state = tracked.get(agent)
if (state?.timer) { clearTimeout(state.timer); state.timer = undefined }
if (!state) {
state = newState()
tracked.set(agent, state)
}
// Freeze the face-shaping switches at this agent's first observation. They
// are volatile row fields, so the host hands an edit to the LIVE row;
// re-reading them here reshaped faces that were already in use (turning the
// pwsh switch off withdrew the own bash tool). The frozen value lasts for this
// agent's whole life; the settings card asks for a restart.
if (state.toolFace === undefined) state.toolFace = {
pwshVisible: deps.pwshEnabled?.() === true,
experimental: deps.experimentalEnabled?.() !== false,
}
const pwshVisible = state.toolFace.pwshVisible === true
const needs = inlineNeeds(names, { pwshVisible })
if (needs.opsPresent) opsSeen = true
// The pwsh face (route B): the host tool is REAL PowerShell now, so the
// default is a product-level HIDE, not a safety denial.
// dsh-ops present -> hands off entirely (their host-pwsh story stands).
Expand Down Expand Up @@ -659,7 +670,7 @@ export function mountInlineTools(ctx, deps) {
// tool exists before step 1 assembles its face. Once created it is kept
// stable for the agent's lifetime; a face that no longer needs our bash
// withdraws it.
const nativeWanted = experimentalEnabled() && (needs.needsBash || state.scope !== undefined || state.pending !== undefined)
const nativeWanted = state.toolFace?.experimental !== false && (needs.needsBash || state.scope !== undefined || state.pending !== undefined)
if (!nativeWanted) disposeNativeSlot(state)
else if (!state.native && !state.nativeFailed) ensureNativeSlot(agent, state)
if (needs.needsBash && !state.scope) {
Expand Down Expand Up @@ -719,11 +730,17 @@ export function mountInlineTools(ctx, deps) {
ctx.on('agent/disposed', ({ agent } = {}) => teardown(agent).catch((error) => {
log(`agent bash tool cleanup failed: ${error?.message ?? error}`)
}))
// A tool-face switch edit arrives as a volatile update on the LIVE row.
// Re-reconciling every tracked agent from here reshaped faces that were
// already in use (it could withdraw the own bash tool). Keep the face an
// agent was created with and say what applies a new one.
ctx.on('loader/volatile-update', (paths) => {
if (!Array.isArray(paths) || !paths.some((path) => Array.isArray(path) && ['pwshTool', 'experimentalNative'].includes(path[0]))) return
for (const agent of tracked.keys()) {
try { reconcile(agent) } catch (error) { log(`pwsh switch update failed: ${error?.message ?? error}`) }
}
if (!Array.isArray(paths)) return
const flipped = paths
.filter((path) => Array.isArray(path) && ['pwshTool', 'experimentalNative'].includes(path[0]))
.map((path) => path[0])
if (flipped.length === 0) return
log(`tool-face switch changed (${flipped.join(', ')}): running agents keep their tool face; a new session or a DSH restart applies the new value`)
})
// MOUNT-TIME SWEEP (v0.35.2): agents that already exist when this row
// mounts never fire agent/created. Same shape the proven dsh-ops denial
Expand All @@ -750,7 +767,12 @@ export function mountInlineTools(ctx, deps) {
if (lazyState && !lazyState.scope && !lazyState.pending) {
try { reconcile(lazyAgent) } catch (error) { log(`lazy re-reconcile failed: ${error?.message ?? error}`) }
}
if (exec?.name === 'pwsh' && !opsSeen && deps.pwshEnabled?.() !== true) {
// Judge the call by the face this agent was created with, never by the
// live switch.
const livePwshOff = deps.pwshEnabled?.() !== true
const frozenFace = exec?.agent ? tracked.get(exec.agent)?.toolFace : undefined
const pwshOff = frozenFace === undefined ? livePwshOff : frozenFace.pwshVisible !== true
if (exec?.name === 'pwsh' && !opsSeen && pwshOff) {
return { kind: 'deny', reason: hiddenPwshDenyReason() }
}
// v0.41.0, second independent layer for the ops shell pair: while our
Expand Down
Loading
Loading