dsh-gitbash-shell 0.39.2 → 0.39.4

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,100 @@
3
3
  > 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
4
4
  > 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
5
5
 
6
+ ## v0.39.4 — 2026-10-14
7
+
8
+ **类型**:feat(dsh-ops 0.3.0 互通:gb_* 退役 + 统一方言桥) / fix(docs)。
9
+
10
+ ### dsh-ops 0.3.0 互通:gb_* 退役 + 双工具家族统一 `/c/` 方言
11
+
12
+ dsh-ops(T-Auto)0.3.0 起以原生 Rust `ops_*` 文件工具(`ops_inspect_local_file`/`ops_grep`/`ops_glob`/`ops_replace`,
13
+ 走 DSH 原生 policy/sandbox/subprocess 通道)取代 FastCtx 上游,并新增 PTC 批量能力(`ops_batch`/`ops_result`,
14
+ opt-in)与跨平台(win/mac/linux)支持。本版把并存形态收敛为「各司其职 + 方言由本插件统一」:
15
+
16
+ - **`fastctxTools` 默认关(弃用声明)**:上游 FastCtx 已被亲爹弃用,`ops_*` 同职能且更强(原生沙箱 mediation、
17
+ 统一 `path` 契约、CAS 原子替换、跨平台)。默认值 true→false(schema、stub reader、live reader、client 卡
18
+ 四处同步);显式开启可恢复,已装 dsh-ops 时无论开关如何都自动让位(既有 `opsPresent` 检测,0.3.0 工具名
19
+ 未变,零改动兼容)。
20
+ - **bash 不让位(产品裁决,2026-10-14)**:dsh-ops 的 `ops_bash` 仅 full-access 会话发布且走裸 `ctx.subprocess`
21
+ (无沙箱 confine、无 unconfined 如实标注);受限会话只有本插件的 bash 可用,且两工具不同名并存无害——
22
+ 本插件继续是「全模式真 Git Bash 沙箱通道」,`ops_bash` 是 full-access 下的第二选择。
23
+ - **入参翻译扩展**:字段白名单加 `root`(ops_batch/ops_result 的路径参数,官方工具无此键,零碰撞);
24
+ 新增 `operations[]` 对象数组的 `path`/`to` 与 `protect[]` 字符串数组的嵌套翻译(同 present `files[].path`
25
+ 的先例)。模型说 `/c/...`,Rust worker 收到原生绝对路径。
26
+ - **出参成功面回流**:ops_* 六件加入 `rewriteResultPaths`——文件四件的 `items[].path`/`matches[].path`
27
+ (Rust 端所有路径字段统一叫 `path`)与 PTC 两件的 `root` 回显 `/c/`;`detailsRef` 等引用 ID 不透明不改写。
28
+ - **失败面回流**:`ERROR_CONTENT_TOOLS` 加 ops_* 六件——失败 content 与 `error.message` 里的 Windows 路径
29
+ 统一回答 `/c/`(`error.message` 走无条件 `msysEcho`,自动覆盖)。
30
+ - **提示词无需改动**:dsh-ops 的提示走 `systemPrompt.section`(与官方同挂点),本插件的 `windowsToMsys`
31
+ 源头替换覆盖全部 sections;其文案无 `C:/` 字面量(「正斜杠分隔符」与 `/c/` 方言天然兼容)。
32
+ - **已知事项**:未安装 dsh-better-sidebar 的部署,启动时宿主对探测式 patch edit 打一条
33
+ `entry "better-sidebar" not found` warn——宿主 patch 引擎对缺失行的固有行为,插件侧无法在保留该 edit
34
+ 功能(boot 层侧栏终端 shell 默认)的前提下去除,README 已说明。
35
+ - **测试**:新增 dsh-ops 互通矩阵(入参 root/operations/protect、出参 items/matches/root、失败 content/message、
36
+ wrapper 门控源码断言、fastctxTools 默认关断言);全量 **172/172**(+1 win32-only skip)。
37
+
38
+ ## v0.39.3 — 2026-10-14
39
+
40
+ **类型**:fix(自有 bash 工具的宿主服务兼容 #27、MSYS 参数/编码提示、源码路径边界与 unconfined 可观测性) / test / docs。
41
+
42
+ ### issue #27:自有 bash 工具在无 `workingDirectory` 服务的宿主上永不注册
43
+
44
+ - **根因(route B 镜像的契约盲区)**:自有 bash 工具镜像的是新版宿主(`0.2.1-alpha.x`)官方 tool-bash
45
+ 的 `inject` 列表(含 `workingDirectory`),而兼容矩阵声明的 `0.2.0-rc.2` 上**没有该服务**——cordis 对
46
+ 无法满足的 inject 让模块 fiber **永久 INACTIVE**:`apply()` 从不执行、工具永不注册、零错误日志,且
47
+ inline-tools 的激活日志照打(`await` 一个 INACTIVE fiber 会正常 resolve)——**假阳性成功**。
48
+ 与 v0.24.0 settingsScope、v0.24.3 静态 peer import 同一失败类(静默失效、宿主日志全绿)。
49
+ - **修复**:① `inject` 收敛为 `['tools', 'systemPrompt', 'shellEnv']`(服务最小面,`workingDirectory`
50
+ 改为使用点 `ctx.get()` 可选读取);② cwd 接缝双向兼容——有 `workingDirectory` 的宿主走
51
+ `ensure()`(与新版官方一致),没有的宿主退回 rc.2 时代官方 `resolveWorkdir` 的同一条接缝
52
+ (`policy.workspaceRoot ?? session.header.cwd`),**不再落到宿主进程目录**;③ 激活日志必须经
53
+ `tools.schemas(agent)` 回读确认 `bash` 真正在册,不在册打显式失败行(点名 INACTIVE 可能性 +
54
+ 请携带 dsh 版本报告)——**消除假阳性**。
55
+ - **教训(§4l 追记⑧)**:镜像宿主模块时,`inject` 面是**契约面**——必须取兼容矩阵内**最老宿主**的
56
+ 形状,或一律可选读取;「宿主新版本有」不等于「矩阵内都有」。
57
+ - 报告者 largoalladonzella 提供了完整根因链(asar 字节级取证 + cordis `_refresh`/`_setEpoch` 源码 +
58
+ 官方两版 inject 对照)与本机验证补丁;本仓库按其建议实现并补真断言。
59
+ - 新增回归:无 `workingDirectory` 的最小 ctx 上 `apply()` 照常注册 + cwd 回退正确;有服务的宿主
60
+ `ensure()` 接缝不变;源码断言锁死硬 inject 列表与「回读门控激活日志」。本地全量 **168/168**(+1
61
+ win32-only skip)。
62
+
63
+ ### PR #24:超时转后台的独立 sandbox 警示
64
+
65
+ - promoted 返回值显式携带可选 sandbox facts,输出 schema 保持闭合;defineTool 与 JSON-schema 回退共用此契约。
66
+ - `renderPromoted` 自行追加 sandbox notes,不再依赖 output 预先含警示;promotion 输出保留丢失/spill 提示,
67
+ 避免重复追加 sandbox 警示。无 sandbox/full-access 的原反馈保持不变,denied/runnerFailed 分支继续保留。
68
+ - 纯回归改用不含警示的原始文本,覆盖空输出、full-access、拒绝与 runner failure。
69
+ 新增实际 ToolRuntime→私有执行器→内存 provider 的 promotion 回归(Jobs wait/read 为受控桩),
70
+ 验证 RO/WW 警示各出现一次、full 无警示、DTO 与最终模型文本分离、丢失/spill 提示及 root 服务不变。
71
+ - 验证:普通完整回归 **166/166**、实际安装Core完整preview **18/18**,均exit0,无skip/cancel;
72
+ 语法与diff检查通过。不等同于生产GUI或真实Jobs时钟验收。
73
+
74
+ ### MSYS 兼容边界尝试修复
75
+
76
+ - **范围**:从 [`6b26ded`](https://github.com/KouzakiUmi/dsh-gitbash-shell/commit/6b26ded6cc3a2a6113579149286047629b433c48)
77
+ 开出 `fix/msys-compatibility-boundaries`;不改 root PowerShell、market patch、生产 Profile、
78
+ 安全默认或版本号。原生参数转换/混合编码/Windows shell confinement 不能在这层安全地普遍修好,
79
+ 因此修工具提示与可观测性,不全局 EXCL=*、不猜码、不将警示冒充沙箱修复。
80
+ - **源码路径**:run_code 的完整 Unicode 字母/组合字符/数字和含 `#` 文件名字面量走共享挂载表;
81
+ 合法 EOF 行注释不再让整份重写弃权;tagged templates(含 String.raw、调用/索引 tag)保持原样,
82
+ 避免 raw 设备路径被错误转义;of/await/yield 等上下文词可作 tag 时保守不改。
83
+ 未闭合/不确定正则与除法仍整份 no-op,未扩展插值或动态表达式猜测。
84
+ 词法层仍无法识别非路径数据的业务用途,提示明确建议直接 Node fs/path 用 native/cwd-relative 路径。
85
+ - **提示门控**:真实 prompt listener 读取 live codePaths;OFF 不再承诺程序字符串会被翻译,
86
+ ON 改为 best-effort 并指出模板、动态/非路径数据限制。关闭 codePaths 不改变工具路径字段翻译。
87
+ - **原生互操作**:登记的 bash 描述无条件说明 `/all`/`/c` 转换、双斜杠/单命令 EXCL 前缀及其
88
+ 对 `/c/...` 的影响;检查预期效果而非只看 exit0。说明 UTF-8 捕获与 CP936/GBK 不匹配,
89
+ 显式转码须在字节捕获前且保持退出状态;locale 与全流猜码不是普遍修复。
90
+ - **unconfined 可见性**:执行句柄在等待 result() 前带 sandbox facts;前台正文、后台 outcome/
91
+ 增量嵌入与超时转后台显示警示。后台登记输出联合新增可选 sandbox facts,指出计划执行边界,
92
+ 不把未启动的 job 宣称为已经执行。保留原请求 mode,不把 unconfined 当 denial/escalation,
93
+ 并明确不得通过 shell 绕过文件工具策略拒绝。双语说明区分文件读能力与 write/edit 围栏,
94
+ 删除“所有未来版本都无解”的过度结论。
95
+ - **验证**:新增9项兼容回归和1项后台登记回归,普通全量 **166/166**;新增3项实际 Core
96
+ ToolRuntime/执行句柄/run_code transport 回归,完整无头 preview **17/17**。旧护栏保留;打包 dry-run、语法和 diff
97
+ 检查通过。新增 preview 使用实际宿主 API 与内存 subprocess,已有 preview 保留真实 Git Bash/
98
+ PowerShell 进程验收;不等同于生产 GUI、真实 PTC 引擎或新 confinement 方案验证。
99
+
6
100
  ## v0.39.2 — 2026-10-13
7
101
 
8
102
  **类型**:fix(route B 工具/预设生命周期、动态配置与客户端回归) / test。
package/README.md CHANGED
@@ -36,10 +36,14 @@ POSIX 宿主上完整保留;
36
36
  会话隐藏,主机保持 bash 优先;开启后它直接可见可用——无需安装任何额外组件。
37
37
  旧 PowerShell 7 旁路工具(v0.37–v0.38.5 的独立工具)已随 v0.39.0 新架构退役;
38
38
  命令仍然首选 bash 工具。
39
- - **`gb_*` FastCtx 文件工具(默认开)**:`gb_inspect_local_file`/`gb_grep`/
39
+ - **`gb_*` FastCtx 文件工具(默认关,v0.39.4 起弃用)**:`gb_inspect_local_file`/`gb_grep`/
40
40
  `gb_glob`/`gb_replace`(Rust 实现:批量范围读取带 token 预算、grep 截断
41
41
  标注、编码探测)。它们是官方 read/grep/glob/edit 的**补充**——官方工具接
42
- 文件沙箱,仍是默认推荐;已安装 dsh-ops 时本组自动让位。
42
+ 文件沙箱,仍是默认推荐。**dsh-ops 0.3.0 起原生 `ops_*` 文件工具取代了
43
+ FastCtx 上游,本组默认关闭**(`fastctxTools` 显式开启可恢复);已安装
44
+ dsh-ops 时无论开关如何都会自动让位。与 dsh-ops 并存时本插件继续负责
45
+ `/c/` 方言统一:`ops_*` 的入参(含 `root`/`operations[].path`/`protect`)
46
+ 自动转换、返回与报错路径自动回流 `/c/`。
43
47
 
44
48
  四个「· Git Bash」**变体 preset 仍保留,默认关闭**(`variantPresets` 开关,
45
49
  设置卡可开):开启后注册 `standard-gitbash`/`minimal-gitbash`/`code-gitbash`/
@@ -118,20 +122,19 @@ bundle patch(`cordis.patch.yml`)是**纯新增**(v0.39.0 起):只插入本插件
118
122
  **Windows 宿主上的事实**:`read-only` / `workspace-write` 权限预设下的 **shell 命令**,
119
123
  实际以**不受限(unconfined)方式执行**:
120
124
 
121
- - **为什么**:MSYS2 运行时无法在 restricted-token 沙箱下启动——`msys-2.0.dll`
122
- 初始化时创建的 cygheap 映射与信号管道,DACL 只含用户 SID;而 WRITE_RESTRICTED
123
- 令牌的二次写检查要求 restricting-SID ACE → 初始化即死于 Win32 error 5 /
124
- 0xC0000142,**任何版本、任何参数都无解**(cmd/pwsh 走匿名管道不受影响)。
125
- 这是 OS 层冲突,插件内没有真正的修法,只有两个选择:**如实标注的绕过**(本插件,
126
- v0.13.0 起)或**直接拒绝执行**。选了前者,是因为装这个插件的目的就是用 Git Bash——
127
- 静默换 pwsh 顶上、或让每条命令都失败,都不如把真相摆在结果里。
128
- - **如何标注(绝不静默)**:每条此类结果的元数据携带
129
- `sandbox: { mode: "<请求的模式>", denied: false, enforcement: "unconfined" }`,
130
- 并在每个进程首次受限调用时打印一行说明日志(`File sandbox enforcement is
131
- partial on this host.`)。
132
- - **文件工具不受影响**:read / write / edit / glob / grep 的沙箱是**另一层**
133
- (fs-tool policy),照常拦截工作区外访问——也就是说 `workspace-write` 下
134
- shell 能写出工作区、文件工具不能,**两者的边界并不一致**。
125
+ - **为什么**:已验证的 Git Bash/MSYS 组合在现有 restricted-token 后端下初始化失败,
126
+ 历史复现为 `msys-2.0.dll` 的 cygheap/signal-pipe 对象权限与 restricting-SID
127
+ 二次写检查不兼容(Win32 error 5 / 0xC0000142)。当前没有提供可用的受限 Git Bash
128
+ 实现,不声称所有未来运行时或隔离方案都无解。本插件保留**如实标注的绕过**;
129
+ 没有静默回退 pwsh/cmd/WSL,本次兼容修复也没有恢复 shell 写入围栏。
130
+ - **如何标注**:结果元数据保留
131
+ `sandbox: { mode: "<请求的模式>", denied: false, enforcement: "unconfined" }`;
132
+ 工具描述、前台正文、后台登记及结果摘要、超时转后台反馈都明确警示 shell 写限制未生效。
133
+ 后台登记标明的是计划采用的执行边界,不代表命令已经运行;实例首次调用另有日志。
134
+ - **文件工具是另一层**:官方 fs-tool policy 对 write/edit 保留策略检查:
135
+ `read-only` 拒绝修改,`workspace-write` 只允许声明的工作区和平台临时可写根。
136
+ read/glob/grep 等读取并非一律限制在工作区内。shell 进程不受这些工具检查约束,
137
+ **两者的边界并不一致**;绝不能用 shell 绕过文件工具已返回的策略拒绝。
135
138
  - **可观察的对照**(相对 pwsh-sandbox 时代):会话进程的完整性级别不再被压低
136
139
  (Low → Medium);`$env:TEMP` 不再被重定向到 per-session 隔离目录。
137
140
 
@@ -396,6 +399,31 @@ PTC/创造及用户自建模式一律覆盖。
396
399
  都不再改写,文件工具收 Windows 路径。**bash 始终是 Git Bash,不受此开关影响**——它只决定
397
400
  跨工具的路径方言。
398
401
 
402
+ ## 原生 Windows 程序与 run_code 的兼容边界
403
+
404
+ 工具描述始终警示下面的参数、编码和安全限制;这些限制不随路径方言关闭而消失。
405
+ 插件**不会自动改写 command、全局关闭 MSYS 参数转换或猜测输出编码**。
406
+
407
+ - **斜杠开关**:MSYS 在启动原生 Windows 程序时可能把 `/all`、`/c` 当路径。
408
+ 本机已验证 `ipconfig //all`、`cmd //c` 或单命令
409
+ `MSYS2_ARG_CONV_EXCL='/all' ipconfig /all` 可保留开关。
410
+ 排除值是前缀:排除 `/c` 也会影响真正的 `/c/...` 路径,该次原生调用应明确用
411
+ Windows drive-letter 路径。不要全局设置 `MSYS2_ARG_CONV_EXCL='*'`。
412
+ `cmd /c` 被转换后可能 exit0 却没有执行所需命令,应检查预期效果而非只看退出码。
413
+ - **输出编码**:宿主按 UTF-8 解码;原生 Windows 程序可能输出 CP936/GBK 或其他代码页。
414
+ 先使用程序自己的 UTF-8 输出选项,或在捕获前按已知编码显式转换字节,保持原退出状态。
415
+ `LANG/LC_ALL` 不保证改变原生程序的代码页。不要全流默认 GBK 或依替换字符数自动猜码:
416
+ 同一 Bash 调用可能混合 UTF-8/GBK,已有乱码字符串也不能可靠恢复原字节。
417
+ - **run_code**:`codePaths` 默认开,与 `posixPaths`/委托 gate 同时生效时,完整、可判定的
418
+ 普通路径字面量按共享挂载表尽力转换,包括中文/组合字符/空格/`#` 文件名。
419
+ EOF 行注释不再阻止转换;tagged template(包括 String.raw)保持原样,避免破坏 raw/tag 语义。
420
+ 插值模板、转义路径、不确定的正则/除法语法仍可能不改;不确定时整份 no-op。
421
+ 直接调用 Node fs/path 时优先明确的 Windows drive-letter 或 cwd-relative 路径;
422
+ 工具 path/file_path 字段仍走工具参数翻译。词法扫描不能识别路由、搜索数据或 shell
423
+ command 的用途;此类字符串不要依赖自动重写,必要时关闭 `codePaths`(提示同步标为 OFF)。
424
+ - **沙箱**:参见上方安全章节。警示可见性得到修复,但 Windows shell 的 unconfined
425
+ 取舍未改变,文件工具的策略拒绝绝不授权 shell 重试。
426
+
399
427
  ## 与 dsh-better-sidebar 联动
400
428
 
401
429
  装了 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar)(v0.15.2+) 时,Windows 上本插件会通过**它的官方设置补丁口**(运行时 `terminalShell` 设置,对方文档明示"settings-page overrides win for terminals opened afterwards")把它打开的终端 shell 指向 Git Bash——**UI 终端标签与模型侧 `terminal_*` 工具统一生效**,不改对方一行代码、新开的终端即生效。此外,**本插件的 bundle patch 还会给对方的行补上 `config.shell`**(启动期解析,连 **tab 标题也会显示 bash**;对方未安装时该行无害跳过)。规则:
package/README_EN.md CHANGED
@@ -10,9 +10,10 @@
10
10
  > on a PRIVATE Git Bash executor instance, gives the four OFFICIAL presets
11
11
  > Git Bash through a per-agent inline tool patch (v0.35.0), keeps the
12
12
  > official `pwsh` tool (REAL PowerShell) hidden behind a switch (`pwshTool`,
13
- > off by default), publishes the `gb_*` FastCtx file
14
- > tools, and keeps the four `· Git Bash` variant presets behind a switch
15
- > (`variantPresets`, off by default).
13
+ > off by default), keeps the `gb_*` FastCtx file tools behind a switch
14
+ > (`fastctxTools`, off by default since v0.39.4 — dsh-ops 0.3.0's native
15
+ > `ops_*` tools superseded FastCtx), and keeps the four `· Git Bash` variant
16
+ > presets behind a switch (`variantPresets`, off by default).
16
17
 
17
18
  ### Host requirement & preset alignment
18
19
 
@@ -60,8 +61,12 @@ The old "replace the executor" three-step patch became runtime work:
60
61
  tool face says "pwsh yes / bash no"; the official `pwsh` tool (REAL
61
62
  PowerShell) stays hidden behind the `pwshTool` switch (off by default);
62
63
  the `gb_*`
63
- FastCtx tools
64
- (`src/fastctx-bridge.js`) publish alongside. With the switch on, it registers the four
64
+ FastCtx tools (`src/fastctx-bridge.js`) publish alongside — **retired to
65
+ an explicit opt-in from v0.39.4** (dsh-ops 0.3.0 replaced FastCtx with
66
+ native `ops_*` file tools; when dsh-ops is present the bridge stands down
67
+ regardless, and this plugin keeps the `/c/` dialect unified across both
68
+ tool families by translating `ops_*` arguments and echoing their results
69
+ and errors back in MSYS form). With the switch on, it registers the four
65
70
  preset variants **declaratively** (`ctx.agentPresets.register`,
66
71
  v0.32.0+) — the row sets are committed, diffable JS data
67
72
  (`src/compositions.js`) and **no directory is ever materialized**;
@@ -93,24 +98,26 @@ restricted tokens, workspace fencing, denial classification, identical to
93
98
  **On Windows**, shell commands under the `read-only` / `workspace-write`
94
99
  permission presets actually run **UNCONFINED**:
95
100
 
96
- - **Why**: the MSYS2 runtime cannot start under the restricted-token sandbox —
97
- `msys-2.0.dll` creates its cygheap mapping and signal pipe with DACLs naming
98
- only the user SID, while a WRITE_RESTRICTED token's pass-2 write check
99
- demands a restricting-SID ACE, so init dies with Win32 error 5 / 0xC0000142
100
- before argv ever runs — **every version, no workaround** (cmd/pwsh use
101
- anonymous pipes and are unaffected). This is an OS-level conflict with no
102
- in-plugin cure; the two options are **a labelled bypass** (this plugin,
103
- since v0.13.0) or **refusing to run at all**. The former was chosen because
104
- installing this plugin means wanting Git Bash — silently falling back to
105
- pwsh, or failing every command, is worse than putting the truth in the result.
106
- - **How it is labelled (never silent)**: every such result carries
107
- `sandbox: { mode: "<requested mode>", denied: false, enforcement: "unconfined" }`,
108
- plus a one-time notice per process (`File sandbox enforcement is partial on
109
- this host.`).
110
- - **File tools are NOT affected**: the read / write / edit / glob / grep
111
- sandbox is a **separate layer** (the fs-tool policy) and still fences
112
- out-of-workspace access. Under `workspace-write` the shell can write outside
113
- the workspace while file tools cannot — **the two boundaries differ**.
101
+ - **Why**: tested Git Bash/MSYS combinations failed to initialize under the
102
+ existing restricted-token backend. Historical reproduction found cygheap/
103
+ signal-pipe object permissions incompatible with the restricting-SID write
104
+ check (Win32 error 5 / 0xC0000142). No working confined Git Bash implementation
105
+ is provided here; this is not a claim that every future runtime or isolation
106
+ design is impossible. The plugin retains its **labelled bypass**, with no
107
+ silent fallback to pwsh/cmd/WSL. Compatibility fixes do not restore confinement.
108
+ - **How it is labelled**: results retain
109
+ `sandbox: { mode: "<requested mode>", denied: false, enforcement: "unconfined" }`.
110
+ The tool description, foreground text, initial background registration,
111
+ background outcome and timeout-promotion feedback warn that shell write
112
+ restrictions are not enforced. Initial registration describes the planned
113
+ boundary, not a claim that the command has already run; there is also a
114
+ one-time notice per executor instance.
115
+ - **Official file tools have a separate policy**: write/edit still reject
116
+ mutations under `read-only`; `workspace-write` checks the declared workspace
117
+ and platform writable temp roots. Reads such as read/glob/grep are not all
118
+ limited to the workspace. Shell processes are not governed by these tool
119
+ checks — **the two boundaries differ**. Never bypass a file-tool policy
120
+ denial through shell.
114
121
  - **Observable differences** vs the pwsh-sandbox era: the session process's
115
122
  integrity level is no longer lowered (Low → Medium), and `$env:TEMP` is no
116
123
  longer redirected to a per-session directory.
@@ -125,6 +132,40 @@ official PowerShell executor was never touched by this plugin. The
125
132
  > The full analysis (the 0.6.0–0.13.1 failure history and the community's two
126
133
  > camps) is in the CHANGELOG under v0.13.2 and in the `src/shell.js` header.
127
134
 
135
+ ## Native Windows and run_code compatibility boundaries
136
+
137
+ The tool description always warns about these argument, encoding and security
138
+ boundaries, independently of the path-dialect switch. The plugin does not
139
+ silently rewrite command source, globally disable argument conversion or guess
140
+ output encodings.
141
+
142
+ - **Slash options**: MSYS can treat native Windows `/all` and `/c` as paths.
143
+ Tested alternatives are `ipconfig //all`, `cmd //c`, or a per-command
144
+ `MSYS2_ARG_CONV_EXCL='/all' ipconfig /all`. Exclusions match prefixes: excluding
145
+ `/c` also excludes `/c/...` file paths, so explicitly use native drive-letter
146
+ paths for those arguments. Do not set `MSYS2_ARG_CONV_EXCL='*'` globally.
147
+ A converted `cmd /c` may exit0 without performing the intended action; verify
148
+ the expected effect, not only the exit code.
149
+ - **Output encodings**: the host decodes UTF-8. Native programs may emit CP936/GBK
150
+ or another code page. Prefer a program's UTF-8 option, or explicitly convert
151
+ known-encoding bytes before capture while preserving the original exit state.
152
+ Locale variables do not guarantee native UTF-8. Do not decode all Bash output
153
+ as GBK or infer encoding from replacement-character counts: one call can mix
154
+ UTF-8 and GBK, and an already-corrupted string cannot recover the original bytes.
155
+ - **run_code**: `codePaths` defaults ON and follows the total/delegate dialect
156
+ gates. Complete compatible plain path literals are rewritten best-effort with
157
+ the shared mount table, including Unicode letters/marks, spaces and `#` names.
158
+ Legal EOF line comments no longer prevent rewriting; tagged templates (such
159
+ as String.raw) retain their data and raw semantics. Interpolation, escapes,
160
+ uncertain regex/division syntax may remain unchanged; uncertainty abandons the
161
+ entire rewrite. Prefer native drive-letter or cwd-relative paths for direct
162
+ Node fs/path APIs; typed tool path arguments remain translated separately.
163
+ Lexical scanning cannot distinguish routes/search data/shell command strings
164
+ from paths. Do not rely on rewriting those values; disable `codePaths` when
165
+ needed (the model hint now accurately states OFF).
166
+ - **Security**: warnings are now visible, but Windows unconfined behavior is
167
+ unchanged. A file-tool policy denial never authorizes a shell retry.
168
+
128
169
  ## Config
129
170
 
130
171
  The `gitbash-shell` row (its id has matched the settings namespace since v0.24.0; it was
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.39.2",
3
+ "version": "0.39.4",
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.2",
3
+ "version": "0.39.4",
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,7 @@
52
52
  "icon.svg"
53
53
  ],
54
54
  "scripts": {
55
- "test": "node --test tests/smoke.mjs tests/private-executor.mjs tests/client-regressions.mjs tests/bridge-regressions.mjs tests/runtime-regressions.mjs tests/host-regressions.mjs",
55
+ "test": "node --test tests/smoke.mjs tests/private-executor.mjs tests/client-regressions.mjs tests/bridge-regressions.mjs tests/runtime-regressions.mjs tests/host-regressions.mjs tests/compatibility-regressions.mjs",
56
56
  "test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs tests/bridge-preview.mjs tests/runtime-preview.mjs tests/host-preview.mjs",
57
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"
58
58
  },
@@ -7,7 +7,7 @@
7
7
  * here). Instead the plugin ships its OWN bash tool whose executor is a private
8
8
  * `GitBashSandboxExecutor` instance (never registered as a service), and this
9
9
  * file mirrors the official tool's rendering contract call-for-call so the
10
- * model sees byte-identical output shapes:
10
+ * model sees the official output shapes, with an explicit unconfined warning:
11
11
  *
12
12
  * - `render.ts` of `@deepseek-ai/dsh-tool-bash` (result / promoted / job-read
13
13
  * text shaping), and
@@ -28,6 +28,17 @@ export function sandboxDenialMarker(mode) {
28
28
  return `[sandbox: file access denied under ${mode} mode]`
29
29
  }
30
30
 
31
+ /** A capability warning, not a denial or permission to bypass another denial. */
32
+ export function sandboxUnconfinedMarker(mode) {
33
+ return `[sandbox: Git Bash is unconfined on Windows — shell file-write restrictions are NOT enforced (requested mode: ${mode}); official file-tool write/edit policy still applies; never bypass its denial through shell]`
34
+ }
35
+
36
+ /** Include the planned execution boundary even before the job has spawned. */
37
+ export function renderBackground(jobId, sandbox) {
38
+ const notes = sandboxNotes(sandbox, [])
39
+ return `started background job ${jobId}${notes.length ? '\n' + notes.join('\n') : ''}`
40
+ }
41
+
31
42
  /** The official same-turn escalation hint (escalation.ts:84). */
32
43
  export function escalationHintMarker(subject) {
33
44
  return `[sandbox: escalation available — retry this exact ${subject} once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`
@@ -84,6 +95,7 @@ export function renderResult(result, escalationModes = []) {
84
95
  markers.push(sandboxDenialMarker(result.sandbox.mode))
85
96
  if (escalationModes.length > 0) markers.push(escalationHintMarker('command'))
86
97
  }
98
+ if (result.sandbox?.enforcement === 'unconfined') markers.push(sandboxUnconfinedMarker(result.sandbox.mode))
87
99
  // A command may trap SIGTERM and exit 0 after timeout; still report interruption.
88
100
  if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`)
89
101
  // A kill from outside the call (the human stopping its job) is not a
@@ -103,16 +115,19 @@ export function renderResult(result, escalationModes = []) {
103
115
  /**
104
116
  * Shape a foreground call that stopped waiting into the text the model sees.
105
117
  * Mirrors the official `renderPromoted` (tool-bash render.ts:77).
106
- * @param {{ jobId: string, timeoutMs: number, output: string }} promoted
118
+ * @param {{ jobId: string, timeoutMs: number, output: string, sandbox?: object }} promoted
119
+ * @param {string[]} [escalationModes]
107
120
  * @returns {string}
108
121
  */
109
- export function renderPromoted(promoted) {
122
+ export function renderPromoted(promoted, escalationModes = []) {
110
123
  const body = promoted.output.length > 0
111
124
  ? (promoted.output.endsWith('\n') ? promoted.output : `${promoted.output}\n`)
112
125
  : ''
113
- return `${body}[still running after ${promoted.timeoutMs}ms; moved to background job ${promoted.jobId}]\n`
126
+ const text = `${body}[still running after ${promoted.timeoutMs}ms; moved to background job ${promoted.jobId}]\n`
114
127
  + 'The command keeps running in the background. You will be notified when it finishes; '
115
128
  + 'read newer output with job_output, stop it with job_kill.'
129
+ const notes = sandboxNotes(promoted.sandbox, escalationModes)
130
+ return notes.length > 0 ? `${text}\n${notes.join('\n')}` : text
116
131
  }
117
132
 
118
133
  /**
@@ -129,6 +144,7 @@ function sandboxNotes(sandbox, escalationModes) {
129
144
  if (escalationModes.length > 0) notes.push(escalationHintMarker('command'))
130
145
  return notes
131
146
  }
147
+ if (sandbox?.enforcement === 'unconfined') return [sandboxUnconfinedMarker(sandbox.mode)]
132
148
  return []
133
149
  }
134
150
 
@@ -192,12 +208,7 @@ export function renderJobRead(delta, lossy, spillPaths, sandbox, escalationModes
192
208
  const paths = Array.isArray(spillPaths) ? spillPaths : []
193
209
  notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(', ') : '(unavailable)'}]`)
194
210
  }
195
- if (sandbox && sandbox.runnerFailed) {
196
- notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`)
197
- } else if (sandbox && sandbox.denied) {
198
- notices.push(sandboxDenialMarker(sandbox.mode))
199
- if (escalationModes.length > 0) notices.push(escalationHintMarker('command'))
200
- }
211
+ notices.push(...sandboxNotes(sandbox, escalationModes))
201
212
  if (notices.length === 0) return delta
202
213
  return `${delta}${delta.length > 0 && !delta.endsWith('\n') ? '\n' : ''}${notices.join('\n')}`
203
214
  }
package/src/bash-tool.js CHANGED
@@ -43,6 +43,7 @@
43
43
  import {
44
44
  ESCALATION_TARGETS,
45
45
  parseExitStatus,
46
+ renderBackground,
46
47
  renderJobRead,
47
48
  renderPromoted,
48
49
  renderResult,
@@ -86,7 +87,7 @@ export async function resolveHostModule(ctx, name) {
86
87
  }
87
88
 
88
89
  /**
89
- * The official bash description (tool-bash index.ts:90), verbatim.
90
+ * The official bash description plus unavoidable Git for Windows boundaries.
90
91
  * @returns {string}
91
92
  */
92
93
  export function bashDescription() {
@@ -97,7 +98,11 @@ export function bashDescription() {
97
98
  + 'Provide `description` before `command` in the arguments. '
98
99
  + 'Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. '
99
100
  + 'An unset variable expands to an empty string, so guard variables in such paths with `${VAR:?}`. '
100
- + 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]`, a policy denial: do not retry another way.'
101
+ + 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]`, a policy denial: do not retry another way. '
102
+ + 'Git for Windows (MSYS) converts path-looking arguments when invoking native Windows programs: ipconfig /all and cmd /c may break; use ipconfig //all or cmd //c, or a per-command MSYS2_ARG_CONV_EXCL prefix. '
103
+ + 'Exclusions are prefixes: excluding /c also excludes /c/... paths, so use native drive-letter paths for those arguments; do not disable conversion globally. Verify the expected effect as well as the exit code. '
104
+ + 'Output is decoded as UTF-8; native Windows programs may emit OEM/ANSI bytes (for example CP936/GBK) and display garbled text. Use a program UTF-8 option or explicit known-encoding conversion before capture; locale variables alone do not fix native output, and mixed streams cannot safely be guessed or decoded globally as GBK. '
105
+ + 'On Windows, this Git Bash route does NOT enforce shell file-write restrictions in read-only/workspace-write mode (unconfined). Official file-tool write/edit policy is separate; never use shell to bypass its denial.'
101
106
  }
102
107
 
103
108
  /** The official foreground/present call view (index.ts:108). */
@@ -187,9 +192,17 @@ const BACKGROUND_OUTPUT_PROPERTIES = {
187
192
  kind: { type: 'string', required: true, const: 'background' },
188
193
  jobId: { type: 'string', required: true },
189
194
  cwd: { type: 'string', required: true },
195
+ sandbox: {
196
+ type: 'object', additionalProperties: false,
197
+ properties: {
198
+ mode: { type: 'string', required: true },
199
+ denied: { type: 'boolean', required: true },
200
+ enforcement: { type: 'string' },
201
+ },
202
+ },
190
203
  }
191
204
 
192
- /** The official output union in the spec dialect defineTool consumes (index.ts:402). */
205
+ /** The official output union, with optional background sandbox facts. */
193
206
  function outputSchemaSpec() {
194
207
  return {
195
208
  oneOf: [
@@ -203,6 +216,10 @@ function outputSchemaSpec() {
203
216
  jobId: { type: 'string', required: true },
204
217
  timeoutMs: { type: 'number', required: true },
205
218
  output: { type: 'string', required: true },
219
+ sandbox: {
220
+ ...BACKGROUND_OUTPUT_PROPERTIES.sandbox,
221
+ properties: { ...BACKGROUND_OUTPUT_PROPERTIES.sandbox.properties, runnerFailed: { type: 'boolean' } },
222
+ },
206
223
  },
207
224
  },
208
225
  {
@@ -319,7 +336,12 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
319
336
  : (subject) => `The narrowest wider sandbox mode for a one-shot retry of the exact ${subject} the sandbox just denied; the retry asks the user for approval.`
320
337
 
321
338
  return {
322
- inject: ['tools', 'systemPrompt', 'shellEnv', 'workingDirectory'],
339
+ // workingDirectory is NOT a service on every host this plugin claims
340
+ // compatibility with (0.2.0-rc.2 has none — issue #27). A missing hard
341
+ // inject leaves the whole module fiber permanently INACTIVE: apply() never
342
+ // runs, the tool never registers, and cordis logs nothing. Read it
343
+ // optionally at the use site (below) instead of injecting it.
344
+ inject: ['tools', 'systemPrompt', 'shellEnv'],
323
345
  apply(ctx, config = {}) {
324
346
  const backgroundEnabled = config?.enableRunInBackground ?? true
325
347
  const promoteOnTimeout = (config?.promoteOnTimeout ?? true) && backgroundEnabled
@@ -424,14 +446,17 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
424
446
  }
425
447
  if (view.status === 'running' || view.status === 'stopping') {
426
448
  const read = registry.read(attached.id, owner)
449
+ const sandbox = attached.process()?.sandbox
427
450
  return {
428
451
  kind: 'promoted',
429
452
  cwd: spec.workdir,
430
453
  jobId: attached.id,
431
454
  timeoutMs,
455
+ // Preserve ring/spill notices; sandbox notes belong to the final renderer.
432
456
  output: renderJobRead(
433
- ringDelta(read.chunks), read.lossy, read.job.output.spillPaths ?? [], attached.process()?.sandbox, escalationModes,
457
+ ringDelta(read.chunks), read.lossy, read.job.output.spillPaths ?? [], undefined, escalationModes,
434
458
  ),
459
+ ...(sandbox === undefined ? {} : { sandbox }),
435
460
  }
436
461
  }
437
462
  registry.remove(attached.id, owner)
@@ -486,7 +511,16 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
486
511
  const policy = approvedMode === undefined
487
512
  ? standingPolicy
488
513
  : { ...standingPolicy, mode: approvedMode }
489
- const cwd = exec.agent === undefined ? undefined : await ctx.workingDirectory.ensure(exec.agent, exec.signal)
514
+ // Optional read (issue #27): on hosts that provide workingDirectory
515
+ // this is the same ensure() seam the modern official tool-bash uses;
516
+ // on hosts without it, fall back to the session cwd exactly the way
517
+ // the rc.2-era official resolveWorkdir did (policy root ?? header).
518
+ const workingDirectory = ctx.get !== undefined ? ctx.get('workingDirectory') : undefined
519
+ const cwd = exec.agent === undefined
520
+ ? undefined
521
+ : workingDirectory !== undefined
522
+ ? await workingDirectory.ensure(exec.agent, exec.signal)
523
+ : (standingPolicy?.workspaceRoot ?? exec.agent?.session?.header?.cwd)
490
524
  const workdir = resolveWorkdir(args.workdir, cwd)
491
525
  const dshEnv = ctx.shellEnv.collect(exec)
492
526
  const request = {
@@ -505,7 +539,15 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
505
539
  }
506
540
  if (exec.signal.aborted) throw toolAborted(llmModule?.HarnessError)
507
541
  const spec = executor.resolve({ ...request, onExpiry: 'none' })
508
- return { kind: 'background', jobId: startJob(jobs, args, exec, spec).id, cwd: spec.workdir }
542
+ // The job may still be preparing: expose the planned route, not a
543
+ // claim that the command has already run or been confined.
544
+ const route = typeof executor.gitBashRoute === 'function' ? executor.gitBashRoute(spec) : undefined
545
+ const sandbox = route === undefined ? undefined : {
546
+ mode: route.mode, denied: false,
547
+ ...(route.unconfined ? { enforcement: 'unconfined' } : {}),
548
+ }
549
+ return { kind: 'background', jobId: startJob(jobs, args, exec, spec).id, cwd: spec.workdir,
550
+ ...(sandbox === undefined ? {} : { sandbox }) }
509
551
  }
510
552
  if (jobs !== undefined && promote) {
511
553
  const spec = executor.resolve({ ...request, onExpiry: 'none' })
@@ -534,9 +576,9 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
534
576
  render: (_args, value) => [{
535
577
  type: 'text',
536
578
  text: value.kind === 'background'
537
- ? `started background job ${value.jobId}`
579
+ ? renderBackground(value.jobId, value.sandbox)
538
580
  : value.kind === 'promoted'
539
- ? renderPromoted(value)
581
+ ? renderPromoted(value, escalationModes)
540
582
  : renderResult(value, escalationModes),
541
583
  }],
542
584
  presentationMeta: (_args, value) => ({ cwd: value.cwd }),
@@ -564,9 +606,9 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
564
606
  render: (_args, value) => [{
565
607
  type: 'text',
566
608
  text: value.kind === 'background'
567
- ? `started background job ${value.jobId}`
609
+ ? renderBackground(value.jobId, value.sandbox)
568
610
  : value.kind === 'promoted'
569
- ? renderPromoted(value)
611
+ ? renderPromoted(value, escalationModes)
570
612
  : renderResult(value, escalationModes),
571
613
  }],
572
614
  presentationMeta: (_args, value) => ({ cwd: value.cwd }),
package/src/client.js CHANGED
@@ -2299,7 +2299,8 @@ window.__ModuleLoader__.load({
2299
2299
  writeField exactly like every other row on this card. */
2300
2300
  var variantOn = snap.value.variantPresets === true;
2301
2301
  var pwshOn = snap.value.pwshTool !== false;
2302
- var fctxOn = snap.value.fastctxTools !== false;
2302
+ // Default OFF (v0.39.4): must match the host's `=== true` reader.
2303
+ var fctxOn = snap.value.fastctxTools === true;
2303
2304
 
2304
2305
  var modeSection = fold("sec.mode",
2305
2306
  E("div", { className: "gb-row" },
package/src/index.js CHANGED
@@ -517,7 +517,7 @@ export const Config = Schema === null ? undefined : Schema.object({
517
517
  * Default ON; Windows-only payload, stands down entirely when dsh-ops is
518
518
  * present (its ops_* tools already cover the same ground).
519
519
  */
520
- fastctxTools: live(Schema.boolean().default(true)),
520
+ fastctxTools: live(Schema.boolean().default(false)),
521
521
  })
522
522
 
523
523
  /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
@@ -582,8 +582,8 @@ function makeLiveReader(ctx, config) {
582
582
  variantPresets: valueOf(config.variantPresets) === true,
583
583
  // Default ON (v0.35.0): the pwsh replacement fills the denied host tool.
584
584
  pwshTool: valueOf(config.pwshTool) !== false,
585
- // Default ON (v0.35.0): the FastCtx bridge stands down when dsh-ops is present.
586
- fastctxTools: valueOf(config.fastctxTools) !== false,
585
+ // Default OFF (v0.39.4, dsh-ops 0.3.0): FastCtx is dead upstream and dsh-ops ships stronger native ops_* file tools; an explicit true re-enables the bridge. It still stands down when dsh-ops is present.
586
+ fastctxTools: valueOf(config.fastctxTools) === true,
587
587
  }
588
588
  }
589
589
  return {
@@ -593,7 +593,7 @@ function makeLiveReader(ctx, config) {
593
593
  autoTerminalShell: () => dialect().autoTerminalShell !== false,
594
594
  variantPresets: () => (hasConfig ? valueOf(config.variantPresets) === true : false),
595
595
  pwshTool: () => (hasConfig ? valueOf(config.pwshTool) !== false : true),
596
- fastctxTools: () => (hasConfig ? valueOf(config.fastctxTools) !== false : true),
596
+ fastctxTools: () => (hasConfig ? valueOf(config.fastctxTools) === true : false),
597
597
  // Default OFF: only an explicit true asks for deduplication.
598
598
  suppressPeerCordis: () => (hasConfig ? valueOf(config.suppressPeerCordis) === true : false),
599
599
  }
@@ -612,15 +612,15 @@ const POSIX_DIRECTIVE_TEXT_STRICT = 'The working shell is Git for Windows bash:
612
612
  * write paths into, so the sentence would be pure noise there. Cheap and
613
613
  * honest: the tool list of the very assembly being rendered decides.
614
614
  */
615
- const RUN_CODE_DIRECTIVE_TEXT = ' Path literals written inside a run_code program are translated the same way before the program runs; shell expansion is not available there, so spell paths out instead of relying on $VARS.'
616
- const RUN_CODE_DIRECTIVE_STRICT_TEXT = ' Path literals written inside a run_code program are translated before the program runs.'
615
+ const RUN_CODE_DIRECTIVE_TEXT = ' In a run_code program, compatible plain MSYS path literals are translated on a best-effort basis; interpolated or tagged templates and uncertain syntax are not rewritten. For direct native Node fs/path APIs, prefer explicit Windows drive-letter or cwd-relative paths. Do not rely on literal rewriting for dynamic paths, shell command strings, routes or search data; tool path arguments are translated separately. Shell expansion is unavailable in program strings, so do not rely on $VARS.'
616
+ const RUN_CODE_DIRECTIVE_OFF_TEXT = ' In a run_code program, automatic path-literal rewriting is OFF. Direct native Node fs/path APIs require Windows drive-letter or cwd-relative paths, not MSYS /c/... strings. Tool path arguments are still translated separately; program strings do not perform shell expansion.'
617
617
 
618
618
  /** The run_code sentence for this assembly, or '' when the tool is not offered. */
619
- export function runCodeHintFor(tools) {
619
+ export function runCodeHintFor(tools, codePaths = true) {
620
620
  if (!Array.isArray(tools)) return ''
621
621
  for (const tool of tools) {
622
622
  const name = tool && typeof tool.name === 'string' ? tool.name : ''
623
- if (name === 'run_code') return RUN_CODE_DIRECTIVE_TEXT
623
+ if (name === 'run_code') return codePaths === true ? RUN_CODE_DIRECTIVE_TEXT : RUN_CODE_DIRECTIVE_OFF_TEXT
624
624
  }
625
625
  return ''
626
626
  }
@@ -773,7 +773,9 @@ function pathEcho(value, env) {
773
773
  * content VERBATIM: output is data, never rewritten (the same red line as
774
774
  * file content in successful results).
775
775
  */
776
- const ERROR_CONTENT_TOOLS = new Set(['read', 'read_image', 'write', 'edit', 'glob', 'grep', 'present'])
776
+ // ops_* six (v0.39.4, dsh-ops 0.3.0): their failures quote Windows paths too —
777
+ // the error content and error.message must answer in the same /c/ dialect.
778
+ const ERROR_CONTENT_TOOLS = new Set(['read', 'read_image', 'write', 'edit', 'glob', 'grep', 'present', 'ops_inspect_local_file', 'ops_glob', 'ops_grep', 'ops_replace', 'ops_batch', 'ops_result'])
777
779
 
778
780
  export function rewriteErrorContent(content, options) {
779
781
  if (!Array.isArray(content)) return content
@@ -868,6 +870,37 @@ export function rewriteResultPaths(name, value, env) {
868
870
  })
869
871
  return changed ? { ...value, files } : value
870
872
  }
873
+ // v0.39.4 (dsh-ops 0.3.0): ops_* tools answer with the same paths the
874
+ // model must keep speaking /c/ about. Their Rust worker names every path
875
+ // field `path` (items[] / matches[]) and ops_batch/ops_result echo the
876
+ // operation `root`; detailsRef ids are opaque, never rewritten.
877
+ if (name === 'ops_grep' && Array.isArray(value.matches)) {
878
+ let changed = false
879
+ const matches = value.matches.map((match) => {
880
+ if (!match || typeof match !== 'object' || typeof match.path !== 'string' || match.path === '') return match
881
+ const out = toMsys(match.path)
882
+ if (out === match.path) return match
883
+ changed = true
884
+ return { ...match, path: out }
885
+ })
886
+ return changed ? { ...value, matches } : value
887
+ }
888
+ if ((name === 'ops_inspect_local_file' || name === 'ops_glob' || name === 'ops_replace') && Array.isArray(value.items)) {
889
+ let changed = false
890
+ const items = value.items.map((item) => {
891
+ if (!item || typeof item !== 'object' || typeof item.path !== 'string' || item.path === '') return item
892
+ const out = toMsys(item.path)
893
+ if (out === item.path) return item
894
+ changed = true
895
+ return { ...item, path: out }
896
+ })
897
+ return changed ? { ...value, items } : value
898
+ }
899
+ if ((name === 'ops_batch' || name === 'ops_result') && typeof value.root === 'string' && value.root !== '') {
900
+ const out = toMsys(value.root)
901
+ if (out !== value.root) return { ...value, root: out }
902
+ return value
903
+ }
871
904
  if (name === 'glob' && Array.isArray(value.paths)) {
872
905
  let changed = false
873
906
  const paths = value.paths.map((p) => {
@@ -909,7 +942,9 @@ export function rewriteResultPaths(name, value, env) {
909
942
 
910
943
  const MSYS_DRIVE_ROOT = /^\/([a-z])\/(.*)$/i
911
944
  const MSYS_BARE_DRIVE = /^\/([a-z])$/i
912
- const TRANSLATABLE_PATH_FIELDS = new Set(['file_path', 'path', 'workdir'])
945
+ // 'root' (v0.39.4): dsh-ops ops_batch/ops_result name their path argument `root`;
946
+ // no official tool uses that key, so the extra alias is collision-free.
947
+ const TRANSLATABLE_PATH_FIELDS = new Set(['file_path', 'path', 'workdir', 'root'])
913
948
 
914
949
  // ── virtual mounts + env (v0.17.0) ─────────────────────────────────────
915
950
  //
@@ -1143,6 +1178,37 @@ export function translatePathArguments(args, env) {
1143
1178
  changed = true
1144
1179
  }
1145
1180
  }
1181
+ // v0.39.4 (dsh-ops 0.3.0): ops_batch carries its paths one level down —
1182
+ // operations[] objects with path/to, and protect[] as a plain string
1183
+ // array. Same deal as present's files[].path: the field-name whitelist
1184
+ // only sees top-level keys. The model speaks /c/ everywhere; these
1185
+ // arrive at the Rust worker as native absolute paths.
1186
+ if (Array.isArray(next.operations)) {
1187
+ const operations = next.operations.map((operation) => {
1188
+ if (!operation || typeof operation !== 'object') return operation
1189
+ let out = operation
1190
+ for (const field of ['path', 'to']) {
1191
+ if (typeof out[field] !== 'string') continue
1192
+ const translated = translateMsysPath(out[field], env)
1193
+ if (translated !== out[field]) out = { ...out, [field]: translated }
1194
+ }
1195
+ return out
1196
+ })
1197
+ if (operations.some((operation, index) => operation !== next.operations[index])) {
1198
+ next.operations = operations
1199
+ changed = true
1200
+ }
1201
+ }
1202
+ if (Array.isArray(next.protect)) {
1203
+ const protect = next.protect.map((entry) => {
1204
+ if (typeof entry !== 'string') return entry
1205
+ return translateMsysPath(entry, env)
1206
+ })
1207
+ if (protect.some((entry, index) => entry !== next.protect[index])) {
1208
+ next.protect = protect
1209
+ changed = true
1210
+ }
1211
+ }
1146
1212
  return changed ? next : args
1147
1213
  }
1148
1214
 
@@ -1155,15 +1221,15 @@ export function translatePathArguments(args, env) {
1155
1221
  // sees it (see AGENTS.md 4c). The same mount table is therefore applied to the
1156
1222
  // program's string literals — but ONLY when the whole program scans cleanly and
1157
1223
  // a literal's ENTIRE content is an absolute MSYS path. Anything unclear — an
1158
- // unterminated literal, a template with ${} interpolation, a regex we cannot
1159
- // place — turns the whole rewrite into a no-op: a half-rewritten program would
1224
+ // unterminated literal or a regex we cannot place — turns the whole rewrite
1225
+ // into a no-op. Interpolated/tagged templates are skipped as data. A partial guess would
1160
1226
  // be far worse than an untranslated one, and this layer never guesses.
1161
1227
  //
1162
1228
  // Deliberately NOT translated: shell-style '$VAR/x' (a JS literal is data, not
1163
1229
  // a shell word) and anything carrying a scheme ('http://...').
1164
1230
 
1165
1231
  /** Source text allowed inside a rewritten literal: no escapes, no dollar sign. */
1166
- const CODE_LITERAL_SAFE = /^[A-Za-z0-9._@+~\-/ ]+$/
1232
+ const CODE_LITERAL_SAFE = /^[\p{L}\p{M}\p{N}._@+~#\-/ ]+$/u
1167
1233
 
1168
1234
  /** Previous significant character that lets a '/' start a regex literal. */
1169
1235
  function regexCanStart(previous) {
@@ -1309,7 +1375,8 @@ export function scanCodeLiterals(code) {
1309
1375
  const next = code[i + 1]
1310
1376
  if (c === '/' && next === '/') {
1311
1377
  const nl = code.indexOf('\n', i)
1312
- if (nl === -1) return null
1378
+ // A line comment may legally terminate at EOF (outside interpolation).
1379
+ if (nl === -1) break
1313
1380
  i = nl
1314
1381
  continue
1315
1382
  }
@@ -1330,7 +1397,12 @@ export function scanCodeLiterals(code) {
1330
1397
  if (c === '`') {
1331
1398
  const template = skipTemplateLiteral(code, i)
1332
1399
  if (template === null) return null
1333
- if (template.plain) spans.push({ start: i + 1, end: template.end - 1 })
1400
+ // A preceding expression can be a tag (String.raw, fn(), obj[key], ...).
1401
+ // Tags own raw/cooked semantics: never rewrite their template data.
1402
+ // Contextual words can also be identifiers/tags; skip rather than guess.
1403
+ if (template.plain && regexCanStart(last) && !['of', 'await', 'yield'].includes(last)) {
1404
+ spans.push({ start: i + 1, end: template.end - 1 })
1405
+ }
1334
1406
  i = template.end
1335
1407
  last = 'x'
1336
1408
  continue
@@ -1489,7 +1561,12 @@ export function translateGlobArguments(args, env) {
1489
1561
  // captured and restored on disposal (reload/update/uninstall), so removing
1490
1562
  // this plugin returns the sidebar to what it was before.
1491
1563
 
1492
- const SIDEBAR_NS = 'dsh-better-sidebar'
1564
+ // The settings namespace is the row id in dsh-better-sidebar's own bundle
1565
+ // patch (`- id: better-sidebar`; verified against the published 0.25.0
1566
+ // tarball) — NOT the npm package name. Using the package name made
1567
+ // settings.describe() never match and every takeover silently no-op with one
1568
+ // "unavailable" log line (issue #25, Bug 3).
1569
+ const SIDEBAR_NS = 'better-sidebar'
1493
1570
  const SIDEBAR_TRIES = 12
1494
1571
  const SIDEBAR_RETRY_MS = 1500
1495
1572
 
@@ -2179,7 +2256,7 @@ export async function apply(ctx, config = {}) {
2179
2256
  // v0.20.1: the run_code sentence rides the SAME context entry, but
2180
2257
  // only when this assembly actually offers the tool (see
2181
2258
  // runCodeHintFor) — every other mode stays quiet.
2182
- const hint = runCodeHintFor(assembly.tools)
2259
+ const hint = runCodeHintFor(assembly.tools, dialect.codePaths === true)
2183
2260
  if (hint !== '') {
2184
2261
  for (const assembled of Array.isArray(assembly.contexts) ? assembly.contexts : []) {
2185
2262
  if (assembled && assembled.name === 'gitbash-shell:posix-paths' && typeof assembled.text === 'string') {
@@ -285,6 +285,18 @@ export function mountInlineTools(ctx, deps) {
285
285
  const fiber = scope.ctx.plugin(module)
286
286
  await untilDisposed(fiber, state.controller.signal)
287
287
  if (!isLive(agent, state)) return
288
+ // An INACTIVE fiber resolves its await() without ever running apply()
289
+ // (issue #27): awaiting the plugin proves nothing, so read the registry
290
+ // back before claiming success. A miss THROWS — the catch below logs the
291
+ // explicit activation failure and the finally disposes this half-mounted
292
+ // scope instead of leaking it for the agent's lifetime (shape credit:
293
+ // KouzakiUmi's PR #28).
294
+ const registeredNames = scope.ctx.get !== undefined
295
+ ? ((scope.ctx.get('tools')?.schemas?.(agent) ?? []).map((schema) => schema && typeof schema.name === 'string' ? schema.name : ''))
296
+ : []
297
+ if (registeredNames.includes('bash') === false) {
298
+ throw new Error('bash tool did not register for this agent (module fiber INACTIVE — this host lacks an injected service the module requires — or the tools read-back is unavailable); bash is NOT available in this session, please report this with your dsh version (issue #27)')
299
+ }
288
300
  retained = true
289
301
  log('own bash tool activated for this agent (Git Bash execution; official pwsh tool real but hidden by default)')
290
302
  } catch (error) {
package/src/shell.js CHANGED
@@ -10,25 +10,23 @@
10
10
  * and hands it to the plugin's OWN bash tool (src/bash-tool.js). The class
11
11
  * body is unchanged from its executor-row era:
12
12
  *
13
- * - extends @deepseek-ai/dsh-bash-sandbox so every confined-mode
14
- * (read-only / workspace-write) call keeps the exact sandbox policy,
15
- * denial, and runner-failure semantics of the shipped stack;
13
+ * - extends @deepseek-ai/dsh-bash-sandbox to inherit policy, denial and
14
+ * lifecycle semantics EXCEPT the explicit Windows unconfined route below;
16
15
  * - overrides `confine` so the inner argv is [git-bash, -c, command];
17
16
  * - overrides run/start ONLY for danger-full-access (the parent's
18
17
  * full-access branch calls LocalBashExecutor.run, which hardcodes the
19
18
  * bare `bash` name), routing through runArgv/startArgv with the same
20
19
  * argv as the confined branch;
21
20
  * - on Windows ALSO overrides the confined branch itself (issue #1):
22
- * MSYS2 cannot start under the restricted-token sandbox — msys-2.0.dll
23
- * init creates its cygheap mapping and signal pipe with DACLs naming
24
- * only the user SID, a WRITE_RESTRICTED token's pass-2 write check
25
- * demands a restricting-SID ACE, so init dies with Win32 error 5 /
26
- * 0xC0000142 before argv ever runs (silent fake-success or a hard
27
- * crash, every version since 0.6.0; cmd/pwsh are unaffected —
28
- * anonymous pipes). OS-level conflict with no in-plugin cure, so
29
- * confined Windows calls run Git Bash UNCONFINED, labelled as such in
30
- * the result (sandbox.enforcement === 'unconfined') plus a one-time
31
- * notice. The fs-tool sandbox policy still applies to file tools.
21
+ * historical MSYS2/restricted-token tests failed during msys-2.0.dll
22
+ * cygheap/signal-pipe initialization (Win32 error 5 / 0xC0000142;
23
+ * restricting-SID object permissions, CHANGELOG v0.13.2). No working
24
+ * confined Git Bash implementation is provided here; this is not a claim
25
+ * that every future runtime/isolation design is impossible. Windows calls
26
+ * retain the explicit UNCONFINED route, labelled on the handle/result and
27
+ * in model-facing foreground/background text, plus a one-time notice.
28
+ * Official file-tool write/edit policy remains separate; it does not
29
+ * constrain shell code and its denials must never be bypassed through shell.
32
30
  *
33
31
  * Environment inheritance: bash.exe is spawned as a direct child of the host
34
32
  * process (never through the git-bash.exe login launcher), so it inherits the
@@ -262,6 +260,8 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
262
260
  * @returns the same handle, with `result()` mapped.
263
261
  */
264
262
  static decorateExecution(ex, sandbox) {
263
+ // Background reads/outcomes inspect the handle before result() settles.
264
+ ex.sandbox = sandbox
265
265
  const base = ex.result.bind(ex)
266
266
  let decorated
267
267
  ex.result = () => {
@@ -407,7 +407,7 @@ export class GitBashSandboxExecutor extends SandboxBashExecutor {
407
407
  warnConfinedUnconfined(mode) {
408
408
  if (this._warnedUnconfined) return
409
409
  this._warnedUnconfined = true
410
- console.log(TAG + ' Windows: MSYS2 cannot start under the restricted-token sandbox (msys-2.0.dll init: cygheap/signal-pipe DACLs carry no restricting-SID ACE -> Win32 error 5 / 0xC0000142; issue #1). Confined call (' + mode + ') ran Git Bash UNCONFINED; the fs-tool sandbox policy still applies.')
410
+ console.log(TAG + ' Windows: confined Git Bash is not provided by this restricted-token backend (issue #1). Requested mode ' + mode + ' will use an UNCONFINED Git Bash route: shell file-write restrictions are NOT enforced. Official file-tool write/edit policy is separate; never bypass its denial through shell.')
411
411
  }
412
412
  }
413
413