dsh-win-multi-bash 0.3.1 → 0.4.0

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/README.i18n.yaml CHANGED
@@ -3,5 +3,5 @@
3
3
  # deepseek-harness per-section format). Both languages carry equal authority;
4
4
  # after editing either side, bring the other along and re-record both hashes:
5
5
  # git hash-object README.md README.zh.md
6
- README.md: ccee00c06fbb2956f5a0559b2b74710e4d4eed55
7
- README.zh.md: bfdcd780500df1a9bd67e9e07aece381cd52e0fd
6
+ README.md: a906eefd6da901187c1a5628e22d9c7b71d4297e
7
+ README.zh.md: c7ff25b1326f99471f2b17f29013bb66e499f4df
package/README.md CHANGED
@@ -39,7 +39,7 @@ The three backends do **not** share the same file-sandbox capability:
39
39
  > **`requireSandbox`: refuse unconfined runs when the probe fails (optional hardening).** Both backends support `requireSandbox: true` (default `false`, keeping the existing degrade-and-run behavior). When enabled, a failed probe (windows-acl unusable for git-bash / bwrap missing for wsl-bash) means: `danger-full-access` runs as usual (an unconfined run is equivalent to an explicit full-access grant), while `read-only` / `workspace-write` calls are **refused** with an error naming the fix and the escalation path. The tool layer also advertises the sandbox and opens the `sandbox_permissions` argument, so the model can take the approval-based escalation. Example:
40
40
 
41
41
  > ```yaml
42
- > # the win-mb-tool-git / win-mb-tool-wsl rows in cordis.patch.yml:
42
+ > # the win-mb-plugin (core row) / win-mb-tool-git / win-mb-tool-wsl rows in cordis.patch.yml:
43
43
  > # each tool row carries only its own backend's partition
44
44
  > - id: win-mb-tool-git
45
45
  > name: 'dsh-win-multi-bash/tool-git-bash'
@@ -70,7 +70,7 @@ wsl.exe -d Ubuntu-24.04 -e bash -c "command -v bwrap && bwrap --version" # ver
70
70
 
71
71
  ## Tool prompts (model-facing descriptions)
72
72
 
73
- The `git_bash` / `wsl_bash` tool descriptions are deliberately concise and mirror the official `tool-pwsh` skeleton: a fresh shell per call, the dialect's paths/env form, `[exit code: N]` markers, `$DSH_*` environment facts, sandbox behavior, output truncation, delete/move target verification, the unset-variable `${VAR:?}` guard, background jobs, and the escalation contract. The longer dialect notes (MSYS path rewriting, WSL base64 payloads) live in this README rather than in the model-facing text.
73
+ Each tool description is deliberately small: it states only what its own dialect owns — the shell and invocation, the dialect's path and environment-variable syntax, and (for `git_bash`) the MSYS note. Everything the family shares is stated once, by the single `tool:win-mb-bash` prompt section owned by the plugin's core row (the package row, `win-mb-plugin`): a fresh shell per call plus the `workdir` rule, `[exit code: N]` markers with the `&&` / `set -o pipefail` gating rule, `$DSH_*` environment facts, sandbox behavior, output truncation, delete/move target verification, the unset-variable `${VAR:?}` guard, background jobs, and the escalation contract. The longer dialect notes (MSYS path rewriting, WSL base64 payloads) live in this README rather than in the model-facing text.
74
74
 
75
75
  Both tools run `bash -c`, so that safety guidance is worded as in `tool-bash`, not as in `tool-pwsh`: `$HOME` is an ordinary assignable variable in bash, so pwsh's "do not assign to automatic variables" sentence would be wrong here — the bash guard for a computed path is the `${VAR:?}` form above, which makes an unset variable fail instead of silently expanding to an empty string.
76
76
 
@@ -82,7 +82,37 @@ So when a command prints an MSYS path (e.g. `/d/WorkSpace/foo`), convert it to i
82
82
 
83
83
  **`workdir` accepts native, MSYS and WSL-automount forms.** A model told that the dialect's paths are MSYS- or WSL-shaped will naturally write `workdir` that way, so the resolver translates a single-letter drive form (`/c/...`) and the WSL automount form (`/mnt/c/...`) into the native `C:\...` before the executor hands the value to `spawn`; MSYS POSIX roots (`/etc`, `/usr`, `/tmp`) and distro-side paths such as `/mnt/data` are left untouched, and a relative `workdir` still resolves against the session workspace. Before that translation an MSYS-form `workdir` failed as `spawn <shell> ENOENT` — a missing-shell symptom for what is really an unusable cwd.
84
84
 
85
- Each tool row also registers a short system-prompt section (`tool:<name>`): check the `[exit code: N]` marker on every result, and chain dependent steps with `&&` or `set -o pipefail` — `;` never stops on failure, and `cmd | tail` returns the status of `tail`, not of `cmd`. That is guidance about *composing* a multi-step command rather than about one call's arguments, so it belongs to the prompt instead of the per-call schema; it also makes the runtime's own tail truncation the reason not to bound output with a pipe.
85
+ **One section for the family, not one per tool.** The `tool:win-mb-bash` section — owned by the package row (the plugin's core row), and by neither tool — carries that shared guidance: check the `[exit code: N]` marker on every result and chain dependent steps with `&&` or `set -o pipefail`, because `;` never stops on failure and `cmd | tail` returns the status of `tail`, not of `cmd`. That is guidance about *composing* a multi-step command rather than about one call's arguments, so it belongs to the prompt instead of the per-call schema; the runtime's own tail truncation is also why a call never needs to bound output with a pipe. Prompt sections live in one global layer keyed by name, so two rows registering the same name throw while two names carrying the same text is duplication — on 0.3.1 the two descriptions were 1299 and 2164 characters with 1117 of them byte-identical, and the exit-status paragraph was assembled twice. The section's text is therefore resolved at every assembly from the tools actually mounted (`ctx.tools.get`), so `git_bash` and `wsl_bash` stay independently switchable: either alone, both, or neither each render exactly one correct copy, and a composition with neither renders no shell guidance at all. Its order is the midpoint of dsh's `TOOL_BASH` / `TOOL_PWSH` section orders, read from `ctx.systemPrompt.getSectionOrder`. The escalation contract and the background sentence are included only while a mounted tool actually advertises `sandbox_permissions` / `run_in_background`. The section never reads facts out of another row's description, so it stays self-sufficient whether or not dsh's `tool-pwsh` is in the composition; the sentences pwsh's own description shares with it are the residue a third-party row cannot edit away.
86
+
87
+ **Row switches: what each combination does.** The bundle ships three rows — the core row plus one per tool — and the Plugins page can switch each one, so the combination space is worth stating. Only one combination changes the *text*; none of them loses information:
88
+
89
+ | `win-mb-plugin` (core row) | `win-mb-tool-git` / `win-mb-tool-wsl` | Result |
90
+ |---|---|---|
91
+ | on | any subset | Intended: each tool states its dialect facts, the core row's section states the shared guidance once, following the mounted subset (a tool that advertises no `sandbox_permissions` also gets no escalation paragraph). |
92
+ | on | **both off** | No family tool is mounted, so the section resolves to empty text and the rendered prompt contains no shell guidance and no shell section at all. The two Configure pages disappear with their rows. |
93
+ | **off** | any subset | The Web client has no package row to find `dsh.client` on, so it serves no browser half and the Plugins page shows no **Configure** control. The tools keep working: with the section gone, each tool description carries the shared guidance itself (the fallback, which also logs a warning). |
94
+
95
+ The core row cannot be marked read-only: `dsh`'s plugin manager locks only the rows from its own protected-module list, its own row, and rows its profile patch cannot address (`readOnlyReason: "management-required" | "unaddressable"`) — there is no manifest flag a third-party bundle can set. The composition is therefore built so that switching the core row off cannot half-break anything: the tool rows are the ones a user actually toggles, and the third row above is the only consequence. Two rows became three: 0.3.0 and 0.3.1 shipped **only** `win-mb-tool-git` and `win-mb-tool-wsl`, so a hand-wired profile that is not migrated still resolves and loads both tools and behaves the same; the missing core row only means the shared guidance falls back into each tool description (above) and that the Plugins-page Configure control is absent — which 0.3.1 did not have either. Re-run `install.ps1` (or add the `win-mb-plugin` row) to get the core row. The separate `win-mb-shell-prompt` row that existed briefly in the development commits between 0.3.1 and 0.4.0 is simply gone — no published version ever declared it, so 0.4.0 ships no module behind that name. A profile that still carries it boots with one inactive entry (the module no longer resolves) and is otherwise unaffected: both tools load, and with no section owner composed each description carries the shared guidance itself.
96
+
97
+ Two further caveats. A row switched off *at runtime* takes effect immediately for the section (it is resolved per assembly), but a tool row that was already loaded keeps the description it registered: the fallback therefore appears only after that tool row reloads, or after a restart. And a warning issued at startup goes through cordis's logger, which the boot collector keeps for its failure report but discards on a successful boot at the default log level — so the fallback text, not the log line, is what actually guarantees the model keeps the guidance.
98
+
99
+ ## Runtime configuration (the Plugins page)
100
+
101
+ Both tool rows carry their own configuration, and every knob is declared `.volatile()` — which is what makes it addressable by the runtime's settings service. The Web **Plugins** page therefore edits it: open **Plugins** in the sidebar, open the `dsh-win-multi-bash` bundle, and each row has a **Configure** page. The browser half (`lib/client.js`, declared as `dsh.client` + the `./client` export) registers that page into the page's `plugins.row.config` slot, keyed `dsh-win-multi-bash#<row id>`; a save goes to the profile's Cordis patch through `settings`/`ctx.configForms` — the same place a hand edit lands, with no HTTP route, no second settings file, and no YAML editing.
102
+
103
+ That is also why the bundle inserts a third row — `win-mb-plugin`, `name: 'dsh-win-multi-bash'` — and why it is the row it is: `dsh-client-modules` discovers a browser half by resolving a row's **bare package specifier** to its `package.json` and reading `dsh.client` there (`locatePkgJson` → `exactPackageSpecifier`, which answers `undefined` for a subpath). The tool rows are subpaths, so without this core row the client bundle is never served and the rows show no Configure control at all. Its module (`lib/index.js`) is the package main, and it owns the shared prompt section as well — one row for the plugin itself, one row per tool.
104
+
105
+ | Row | Options (in page order) |
106
+ |---|---|
107
+ | `git_bash` | Background jobs (`enableRunInBackground`); `cwd`, `timeoutMs`, `maxTimeoutMs`, `maxOutputBytes`, `maxSpillBytes`, `graceMs`; `bashPath`; sandbox stance (`auto` / `none`); `probeTimeoutMs`; `requireSandbox` |
108
+ | `wsl_bash` | The same set under `wslBash`, plus `wslPath` and `wslDistro`, and the stance list adds `bwrap` |
109
+
110
+ That list is exactly the volatile projection of each row's `Config` schema — the audit pins both sides (`volatilePaths` of the host configs against the browser half's field table), so a knob added to a backend without `.volatile()` cannot silently stay YAML-only, and the page cannot offer a field the Host would refuse.
111
+
112
+ - **Live reads.** Volatile fields are live references, and the executors read them through `.get()` at use time. A path, distro, stance, timeout, or hardening change applies to the next command without reloading the row.
113
+ - **Two knobs need the row's next load**, because they shape the tool's *schema* rather than one call: `enableRunInBackground` (the advertised `run_in_background` argument) and the escalation surface (`sandbox_permissions`, advertised at load from the probe verdict).
114
+ - **Staged, not live typing.** The page stages drafts and writes only on **Save**, fenced by the revision it read; a save the Host refuses keeps the drafts. Emptying a path or a distro stages a clear, so the override drops and the built-in probe resolves again.
115
+ - The page exists while the Host serves that row's namespace (`whileServed`), so a row that is switched off shows no Configure control.
86
116
 
87
117
  ## Path conversion (MSYS auto-rewriting)
88
118
 
@@ -132,7 +162,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -ProfileName <name>
132
162
  powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
133
163
  ```
134
164
 
135
- The script links the package into `<profile>/node_modules/` (a junction), maintains the package-local `node_modules/@deepseek-ai` junction the bundled code needs, and writes a managed block into the profile's `cordis.patch.yml` — `dsh web` hot-reloads that file, so the feature goes live without a restart. The script is idempotent and auto-detects Git Bash installs outside the default probe paths by reading `HKLM:\SOFTWARE\GitForWindows` and pinning `gitBash.bashPath`.
165
+ The script links the package into `<profile>/node_modules/` (a junction), maintains the package-local `node_modules/@deepseek-ai` junction the bundled code needs, and writes a managed block into the profile's `cordis.patch.yml` — `dsh web` hot-reloads that file, so the feature goes live without a restart. The script is idempotent and auto-detects Git Bash installs outside the default probe paths by reading `HKLM:\SOFTWARE\GitForWindows` and pinning `gitBash.bashPath`. Both scripts ship inside the published package too, so a registry install can run them straight from `node_modules/dsh-win-multi-bash/`.
136
166
 
137
167
  ### Path B: bundle install (portable, requires restart)
138
168
 
@@ -180,10 +210,11 @@ Runs the full audit suite instead: pure-unit coverage of the vendor helpers, exe
180
210
 
181
211
  ```
182
212
  dsh-win-multi-bash/
183
- ├── package.json # dsh.bundle manifest; exports ./tool-git-bash ./tool-wsl-bash
213
+ ├── package.json # dsh.bundle + dsh.client manifest; exports . ./client ./tool-git-bash ./tool-wsl-bash
184
214
  ├── cordis.patch.yml # the composition wiring (documented inline)
185
215
  ├── install.ps1 # Path A hot plug (junctions + managed block + Git Bash detection)
186
216
  ├── uninstall.ps1 # Path A hot unplug
217
+ ├── .gitattributes # LF-only for every text file, binaries exempt
187
218
  ├── LICENSE / THIRD_PARTY_NOTICES
188
219
  ├── lib/ # bundled implementation (plain ESM JS, no build step)
189
220
  └── smoke/ # smoke test (not published)
package/README.zh.md CHANGED
@@ -70,7 +70,7 @@ wsl.exe -d Ubuntu-24.04 -e bash -c "command -v bwrap && bwrap --version" # 验
70
70
 
71
71
  ## 工具提示词(面向模型的描述)
72
72
 
73
- `git_bash` / `wsl_bash` 的工具描述刻意保持精简,与官方 `tool-pwsh` 同构:每次调用全新 shell、方言的路径/环境变量写法、`[exit code: N]` 标记、`$DSH_*` 环境事实、沙箱行为、输出截断、删除/移动前的目标路径校验、未设变量的 `${VAR:?}` 兜底、后台任务与升级契约。更长的方言说明(MSYS 路径改写、WSL base64 载荷)放在本文档而不是模型可见的描述里。
73
+ `git_bash` / `wsl_bash` 的工具描述刻意保持最小:只写各自方言自身的内容——shell 与调用形式、方言的路径与环境变量写法、(`git_bash` 还带)MSYS 路径注记。两者共有的内容由插件核心行(包行 `win-mb-plugin`)拥有的**唯一**一段 `tool:win-mb-bash` 提示词承载:每次调用全新 shell 与 `workdir` 规则、`[exit code: N]` 标记及 `&&` / `set -o pipefail` 串接规则、`$DSH_*` 环境事实、沙箱行为、输出截断、删除/移动前的目标路径校验、未设变量的 `${VAR:?}` 兜底、后台任务与升级契约。更长的方言说明(MSYS 路径改写、WSL base64 载荷)放在本文档而不是模型可见的描述里。
74
74
 
75
75
  这两个工具都用 `bash -c`,因此上述安全提示采用 `tool-bash` 的 bash 版措辞而非 `tool-pwsh` 的:bash 里 `$HOME` 是可赋值的普通变量,pwsh 的「不要给自动变量赋值」那句在此会误导;bash 对计算路径的兜底就是上面的 `${VAR:?}` 写法——它让未设变量直接报错,而不是静默展开成空串。
76
76
 
@@ -82,7 +82,37 @@ wsl.exe -d Ubuntu-24.04 -e bash -c "command -v bwrap && bwrap --version" # 验
82
82
 
83
83
  **`workdir` 接受原生、MSYS 与 WSL 挂载三种写法。** 模型在被告知方言路径是 MSYS/WSL 形式后,自然会用同样的形式写 `workdir`;解析器因此把单字母盘符形式(`/c/...`)与 WSL 挂载形式(`/mnt/c/...`)在交给 `spawn` 之前转成原生 `C:\...`,而 MSYS 的 POSIX 根(`/etc`、`/usr`、`/tmp`)与发行版侧路径(如 `/mnt/data`)保持原样,相对路径仍相对会话工作区解析。在此转换之前,MSYS 形式的 `workdir` 会以 `spawn <shell> ENOENT` 失败——一个「找不到 shell」的假象,实际是 cwd 不可用。
84
84
 
85
- 工具行还会注册一段系统提示词小节(`tool:<name>`):每次结果都要核对 `[exit code: N]` 标记,且依赖前一步的后续命令要用 `&&` 或 `set -o pipefail` 串接——`;` 不会因失败中止,而 `cmd | tail` 返回的是 `tail` 的状态而非 `cmd` 的。这属于「如何组合多步命令」的跨调用指导,因此放在提示词里而不是单次调用的 schema 里;它也让运行时自带的截尾能力成为「不必用管道限制输出」的理由。
85
+ **整个家族只注册一段提示词小节,而不是每个工具一段。** 该小节(`tool:win-mb-bash`,由**包行/核心行**拥有,不属于任何一个工具)承载上述共有指导:每次结果都要核对 `[exit code: N]` 标记,且依赖前一步的后续命令要用 `&&` 或 `set -o pipefail` 串接——`;` 不会因失败中止,而 `cmd | tail` 返回的是 `tail` 的状态而非 `cmd` 的。这属于「如何组合多步命令」的跨调用指导,因此放在提示词里而不是单次调用的 schema 里;它也让运行时自带的截尾能力成为「不必用管道限制输出」的理由。提示词小节存放在**按名字索引的单一全局层**里:两个行注册同一个名字会直接抛错,而两个名字装同样的文本就是重复——0.3.1 上两段描述分别为 1299 与 2164 字符、其中 1117 字符逐字节相同,退出码那段还被装配了两次。因此小节的文本在**每次装配时**按实际挂载的工具重新解析(`ctx.tools.get`):`git_bash` 与 `wsl_bash` 保持可独立开关,只挂其一、两者都挂、或都不挂,各自都只渲染出恰好一份正确文本(都不挂时不渲染任何 shell 指导)。其排序取 dsh `TOOL_BASH` / `TOOL_PWSH` 两个段位的中值(读自 `ctx.systemPrompt.getSectionOrder`)。升级契约段与后台任务句只在实际挂载的工具确实声明了 `sandbox_permissions` / `run_in_background` 时才出现。该小节从不从别的行的描述里取事实,因此 dsh 的 `tool-pwsh` 在或不在组合里它都自足;pwsh 自身描述与它重合的句子,是第三方行无法删掉的残留。
86
+
87
+ **行开关:各组合的实际行为。** 接线块一共三行——核心行 + 每个工具一行——面板上都能单独开关,所以组合空间值得写清楚。只有一个组合会改变**文本**形态,且没有任何组合会丢信息:
88
+
89
+ | `win-mb-plugin`(核心行) | `win-mb-tool-git` / `win-mb-tool-wsl` | 结果 |
90
+ |---|---|---|
91
+ | 开 | 任意子集 | 目标形态:工具只说自己方言的事实,共有指导由核心行的小节说一次,并随实际挂载的子集变化(不广告 `sandbox_permissions` 的工具也不会看到升级段)。 |
92
+ | 开 | **都关** | 没有家族工具被挂载,小节解析为空文本,渲染出的提示词里既没有那段指导、也没有这一节。两个配置页随各自的行走。 |
93
+ | **关** | 任意子集 | Web 端没有包行可解析 `dsh.client`,于是不下发浏览器半边,面板上没有 **配置** 入口。工具照旧可用:小节不在了,共有指导改由每个工具自己的描述承载(兜底,同时记一条警告)。 |
94
+
95
+ **核心行无法被标成只读**:dsh 的插件管理只锁三类行——它自己的"受保护模块"名单里的行、它自己那一行、以及它的 profile patch 无法唯一定位的行(`readOnlyReason: "management-required" | "unaddressable"`);第三方组合包没有任何清单字段可以声明"本行只读"。因此这里的做法是让"关掉核心行"不可能造成半坏状态:用户真正要开关的是两个工具行,而上面第三行就是关掉核心行后的全部后果。两行变三行:0.3.0 / 0.3.1 **只**交付过 `win-mb-tool-git` 与 `win-mb-tool-wsl`,因此未迁移的手工接线 profile 仍然能解析并加载这两个工具、行为不变;缺的只是核心行——共有指导会回落到各自描述里(见上),并且面板上没有 **配置** 入口(0.3.1 本来也没有)。重跑 `install.ps1`(或补上 `win-mb-plugin` 行)即可拿到核心行。0.3.1 与 0.4.0 之间的开发提交里短暂存在过的独立 `win-mb-shell-prompt` 行**已直接删除**——任何已发布版本都不曾声明它,0.4.0 也不再为该名字提供模块。若你的 profile 仍带着它,启动时会报一个"未激活条目"(模块无法解析),此外不受影响:两个工具照常加载,且因为没有小节拥有者,各自描述会自带那段共有指导。
96
+
97
+ 两点注意:**运行时**关掉某行对小节立即生效(每次装配都重新解析),但已加载的工具行仍保留它注册时的描述,因此"关掉核心行→兜底接管"要等该工具行重载或重启才发生。另外,启动期经 cordis logger 发出的警告,会被 app-boot 的收集器留着用于失败报告、但在成功启动且默认日志级别下丢弃——所以**真正保证模型不丢指导的是兜底文本,而不是那条日志**。
98
+
99
+ ## 运行时配置(插件管理面板)
100
+
101
+ 两个工具行各自携带配置,且每个可配置项都声明了 `.volatile()`——这正是运行时设置服务能够寻址它们的条件。因此 Web 侧栏 **插件** 页可以直接编辑:侧栏打开 **插件** → 打开 `dsh-win-multi-bash` 包 → 每个行都有 **配置** 页。该页由浏览器半边(`lib/client.js`,通过 `dsh.client` 与 `./client` 导出声明)注册进页面的 `plugins.row.config` 槽位,key 为 `dsh-win-multi-bash#<行 id>`;保存经 `settings` / `ctx.configForms` 写入 profile 的 Cordis patch——与手改落点相同,没有自建 HTTP 路由、没有第二份设置文件,也不用碰 YAML。
102
+
103
+ 这也是接线块要插入第三行的原因(`win-mb-plugin`,`name: 'dsh-win-multi-bash'`):`dsh-client-modules` 发现浏览器半边的方式是把行的**裸包名**解析到其 `package.json` 再读 `dsh.client`(`locatePkgJson` → `exactPackageSpecifier`,对子路径返回 `undefined`)。两个工具行都是子路径,缺了这一行浏览器半边永远不会被服务,行上也就没有任何配置入口。它的模块(`lib/index.js`)就是包 main,同时拥有那段共有提示词小节——**插件本身一行,每个工具一行**。
104
+
105
+ | 行 | 可配置项(按页面顺序) |
106
+ |---|---|
107
+ | `git_bash` | 后台任务(`enableRunInBackground`);`cwd`、`timeoutMs`、`maxTimeoutMs`、`maxOutputBytes`、`maxSpillBytes`、`graceMs`;`bashPath`;沙箱立场(`auto` / `none`);`probeTimeoutMs`;`requireSandbox` |
108
+ | `wsl_bash` | `wslBash` 下同一组,外加 `wslPath`、`wslDistro`,且沙箱立场多一个 `bwrap` |
109
+
110
+ 该列表就是每个行 `Config` schema 的 volatile 投影——审计把两侧对齐(主机 config 的 `volatilePaths` 对浏览器半边的字段表),所以给后端加了字段却忘了 `.volatile()` 不会悄悄只留在 YAML,页面也不可能提供 Host 会拒绝的字段。
111
+
112
+ - **活引用**:volatile 字段是活引用,执行器在使用时通过 `.get()` 读取。路径、发行版、沙箱立场、超时与强化开关的改动对下一条命令即生效,无需重载该行。
113
+ - **两项需要该行下次加载**(因为它们塑造的是工具 **schema** 而不是单次调用):`enableRunInBackground`(对外广告的 `run_in_background` 参数)与升级面(`sandbox_permissions`,加载时按探针结论广告)。
114
+ - **暂存而非即输即写**:页面暂存草稿,仅 **保存** 时写入,并以读取时的 revision 做栅栏;Host 拒绝的保存会保留草稿。清空路径或发行版会提交一次 clear,撤销覆盖并让内置探测重新生效。
115
+ - 该页仅在 Host 服务该行命名空间时存在(`whileServed`),因此被关掉的行不会显示配置入口。
86
116
 
87
117
  ## 路径转换(MSYS 自动改写)
88
118
 
@@ -132,7 +162,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -ProfileName <name>
132
162
  powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
133
163
  ```
134
164
 
135
- 脚本把包链接进 `<profile>/node_modules/`(junction),维护包内 `node_modules/@deepseek-ai` junction(打包代码解析基础包所需),并把 managed 接线块写入 profile 的 `cordis.patch.yml`——`dsh web` 热重载该文件,**立即生效,无需重启**。脚本幂等,并会自动检测默认探测路径之外的 Git Bash(读取 `HKLM:\SOFTWARE\GitForWindows` 写入 `gitBash.bashPath`)。
165
+ 脚本把包链接进 `<profile>/node_modules/`(junction),维护包内 `node_modules/@deepseek-ai` junction(打包代码解析基础包所需),并把 managed 接线块写入 profile 的 `cordis.patch.yml`——`dsh web` 热重载该文件,**立即生效,无需重启**。脚本幂等,并会自动检测默认探测路径之外的 Git Bash(读取 `HKLM:\SOFTWARE\GitForWindows` 写入 `gitBash.bashPath`)。两个脚本也随 npm 包一起发布,因此从 registry 安装的用户可以直接在 `node_modules/dsh-win-multi-bash/` 下运行。
136
166
 
137
167
  ### 方式 B:bundle 安装(便携,需重启)
138
168
 
@@ -180,10 +210,11 @@ powershell -ExecutionPolicy Bypass -File .\smoke\audit.ps1
180
210
 
181
211
  ```
182
212
  dsh-win-multi-bash/
183
- ├── package.json # dsh.bundle 清单;exports 暴露 ./tool-git-bash ./tool-wsl-bash
213
+ ├── package.json # dsh.bundle + dsh.client 清单;exports 暴露 . ./client ./tool-git-bash ./tool-wsl-bash
184
214
  ├── cordis.patch.yml # 组合接线(即文档)
185
215
  ├── install.ps1 # 方式 A 热插(junction + managed 块 + Git Bash 检测)
186
216
  ├── uninstall.ps1 # 方式 A 热拔
217
+ ├── .gitattributes # 所有文本文件 LF,二进制例外
187
218
  ├── LICENSE / THIRD_PARTY_NOTICES
188
219
  ├── lib/ # 打包实现(纯 ESM JS,无构建步骤)
189
220
  └── smoke/ # 冒烟测试(不随包发布)
package/cordis.patch.yml CHANGED
@@ -1,24 +1,57 @@
1
1
  # dsh-win-multi-bash — self-contained Windows multi-bash wiring.
2
2
  #
3
3
  # This patch is the whole plugin: it inserts the plugin's own rows. Every
4
- # inserted row loads code from THIS package (exports ./tool-git-bash,
5
- # ./tool-wsl-bash) — nothing depends on runtime packages beyond the published
6
- # @deepseek-ai base packages (dsh-bash-local, dsh-sandbox, dsh-tools,
7
- # dsh-shell, dsh-llm, ...).
4
+ # inserted row loads code from THIS package (exports ., ./client,
5
+ # ./tool-git-bash, ./tool-wsl-bash) — nothing depends on runtime packages
6
+ # beyond the published @deepseek-ai base packages (dsh-bash-local, dsh-sandbox,
7
+ # dsh-tools, dsh-shell, dsh-llm, ...).
8
8
  #
9
9
  # ── What the rows do ─────────────────────────────────────────────────────────
10
+ # win-mb-plugin — the package row: the plugin's core row. `name` is the
11
+ # BARE package specifier and the module is the package
12
+ # main, which does two things.
13
+ # (1) It carries the browser half. The Web client
14
+ # discovers one by resolving a row's package name to
15
+ # its package.json (dsh-client-modules:
16
+ # locatePkgJson → exactPackageSpecifier, which
17
+ # answers `undefined` for a subpath) and reading
18
+ # `dsh.client` + the `./client` export there. The
19
+ # tool rows are subpaths, so without this row
20
+ # `lib/client.js` is never served and the tool rows
21
+ # show no Configure page on the Plugins page.
22
+ # (2) It owns the ONE prompt section the two bash tools
23
+ # share (`tool:win-mb-bash`, order = midpoint of
24
+ # dsh's TOOL_BASH/TOOL_PWSH). Its text is resolved
25
+ # per assembly from the tools actually mounted, so
26
+ # it says nothing when neither tool is enabled and
27
+ # never assumes the other tool — or pwsh — is
28
+ # present. Shared guidance lives here once instead
29
+ # of once per tool description; each tool
30
+ # description keeps only its own dialect facts.
31
+ # This is the one row that must not be switched off:
32
+ # there is no supported way to mark a third-party row
33
+ # read-only (dsh's plugin manager locks only rows from
34
+ # its own protected-module list, its own row, and rows
35
+ # it cannot address), so instead the composition is
36
+ # built so that nothing can half-break: switch it off
37
+ # and the tools keep working — their descriptions fall
38
+ # back to carrying the shared guidance themselves — and
39
+ # only the Plugins-page configuration disappears.
10
40
  # win-mb-tool-git — registers the model-facing `git_bash` tool
11
41
  # (MSYS dialect) into the host tools registry: every
12
42
  # session sees it regardless of its agent preset. The
13
43
  # tool owns its own GitBashExecutor; its model-facing
14
- # description mirrors the official tool-pwsh skeleton
15
- # (concise) and reminds the model that MSYS paths are
16
- # valid only inside Git Bash — dsh's file tools on
17
- # Windows take native C:\... paths.
44
+ # description carries only the MSYS facts and reminds
45
+ # the model that MSYS paths are valid only inside Git
46
+ # Bash — dsh's file tools on Windows take native
47
+ # C:\... paths.
18
48
  # win-mb-tool-wsl — the `wsl_bash` tool, owning its own WslBashExecutor;
19
- # its description mirrors tool-pwsh's concise skeleton.
49
+ # its description carries only the WSL facts.
20
50
  #
21
- # Both rows are win32-only; POSIX keeps the base bundle's own bash wiring.
51
+ # All three rows are win32-only; POSIX keeps the base bundle's own bash wiring.
52
+ # The tool rows are independent of each other and of the core row — the section
53
+ # follows whichever subset is mounted, and the core row's absence degrades to
54
+ # the fallback described above rather than to a broken prompt.
22
55
  #
23
56
  # ── Why nothing here touches the ctx.shell seat ──────────────────────────────
24
57
  # dsh 0.1.7 removed the shell seam's routing field (`ShellExecRequest.shell`),
@@ -36,6 +69,15 @@
36
69
  # itself rather than by us.
37
70
 
38
71
  - insert:
72
+ # The core row. Bare specifier on purpose: this is the row that lets the Web
73
+ # client find this package's `dsh.client` declaration, and the row that owns
74
+ # the shared prompt section (see the header note). Keep it enabled — with it
75
+ # off the tools still run and still carry the shared guidance themselves, but
76
+ # the Plugins page loses its Configure pages.
77
+ - id: win-mb-plugin
78
+ name: 'dsh-win-multi-bash'
79
+ disabled: !!js process.platform !== 'win32'
80
+
39
81
  - id: win-mb-tool-git
40
82
  name: 'dsh-win-multi-bash/tool-git-bash'
41
83
  disabled: !!js process.platform !== 'win32'
package/install.ps1 ADDED
@@ -0,0 +1,179 @@
1
+ <#
2
+ .SYNOPSIS
3
+ 热插 dsh-win-multi-bash(自包含版):把插件链接进 profile 的 node_modules,
4
+ 并把接线块写入 profile 的 cordis.patch.yml。dsh web 会热重载该文件,无需重启。
5
+
6
+ .DESCRIPTION
7
+ 本插件自带全部功能实现(lib/ 下打包的执行器与工具实例),只依赖已发布的
8
+ @deepseek-ai 基础包。脚本做两件事:
9
+
10
+ 1) 在 <profile>/node_modules/dsh-win-multi-bash 建立指向本插件目录的
11
+ junction,让组合行(name: 'dsh-win-multi-bash/...')能解析到本包代码;
12
+ 2) 把 managed 接线块写入 profile 的 cordis.patch.yml(幂等:先删旧块再生成)。
13
+
14
+ 接线块是统一的:插入本插件的三行——win-mb-plugin(核心行/包自身那一行,
15
+ name 用裸包名:供 Web 端发现 dsh.client 并加载浏览器半边,同时拥有两个
16
+ bash 工具共有的那段系统提示词,只注册一次)、win-mb-tool-git /
17
+ win-mb-tool-wsl。
18
+ 每个工具自带执行器(0.1.7 起 shell 接缝已无 request.shell 路由字段,
19
+ 选择器无从路由),不占用 ctx.shell 席位;基座自带的 pwsh-sandbox 行
20
+ 继续提供 shell 席位,pwsh 行为与未装插件时一致。
21
+
22
+ 可选:检测到本机 Git Bash 位于运行时默认探测路径之外(如非默认盘符的
23
+ 安装目录,或 PATH 里只有 C:\WINDOWS\system32\bash.exe 这个 WSL 启动器)
24
+ 时,自动在 git_bash 工具行的 config.gitBash.bashPath 写入覆盖(探测所得
25
+ 的真实路径,非硬编码),保证 git_bash 工具用上真正的 MSYS bash。
26
+
27
+ .PARAMETER ProfileName
28
+ 目标 profile 名,默认 web(即 ~/.dsh/profiles/web)。
29
+
30
+ .PARAMETER Force
31
+ 即使检测到该插件已作为 bundle 装入 profile(dsh plugin add 路径),也继续。
32
+ 两种路径同时存在会导致组合行重复,正常情况下应互斥。
33
+ #>
34
+ [CmdletBinding()]
35
+ param(
36
+ [string]$ProfileName = 'web',
37
+ [switch]$Force
38
+ )
39
+
40
+ $ErrorActionPreference = 'Stop'
41
+
42
+ $pluginRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
43
+ $dshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFILE '.dsh' }
44
+ $profileDir = Join-Path $dshHome "profiles\$ProfileName"
45
+ $patchPath = Join-Path $profileDir 'cordis.patch.yml'
46
+ $profileNodeModules = Join-Path $profileDir 'node_modules'
47
+ $linkPath = Join-Path $profileNodeModules 'dsh-win-multi-bash'
48
+
49
+ if (-not (Test-Path $profileDir)) { throw "profile 不存在: $profileDir" }
50
+ if (-not (Test-Path $patchPath)) { throw "profile patch 不存在: $patchPath" }
51
+
52
+ # ── 0. 互斥检查:bundle 路径(dsh plugin add)与本热插路径不能同时使用 ─────
53
+ $manifestPath = Join-Path $profileDir 'package.json'
54
+ if (-not $Force -and (Test-Path $manifestPath)) {
55
+ $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
56
+ if ($manifest.dependencies.PSObject.Properties.Name -contains 'dsh-win-multi-bash') {
57
+ throw '检测到 dsh-win-multi-bash 已作为 bundle 装入本 profile(dsh plugin add 路径)。' +
58
+ '两种插拔路径互斥(组合行会重复导致 loader 报 duplicate entry id)。' +
59
+ '请先用 `dsh plugin --profile <name> remove dsh-win-multi-bash` 卸载 bundle,' +
60
+ '或加 -Force 强制继续(不推荐)。'
61
+ }
62
+ }
63
+
64
+ # ── 1. 把插件链接进 profile node_modules(组合行解析 'dsh-win-multi-bash/...') ─
65
+ if (-not (Test-Path $profileNodeModules)) {
66
+ New-Item -ItemType Directory -Path $profileNodeModules -Force | Out-Null
67
+ }
68
+ if (Test-Path $linkPath) {
69
+ $item = Get-Item $linkPath -Force
70
+ if ($item.LinkType -eq 'Junction') {
71
+ if ($item.Target -ieq $pluginRoot) {
72
+ Write-Host "[dsh-win-multi-bash] 插件 junction 已存在且指向正确: $linkPath"
73
+ } else {
74
+ Write-Host "[dsh-win-multi-bash] 插件 junction 指向旧位置 ($($item.Target)),重建"
75
+ cmd /c rmdir "$linkPath"
76
+ New-Item -ItemType Junction -Path $linkPath -Target $pluginRoot | Out-Null
77
+ }
78
+ } else {
79
+ Write-Host "[dsh-win-multi-bash] $linkPath 已存在但不是 junction,跳过(请手动确认其内容)"
80
+ }
81
+ } else {
82
+ New-Item -ItemType Junction -Path $linkPath -Target $pluginRoot | Out-Null
83
+ Write-Host "[dsh-win-multi-bash] 已建立插件 junction: $linkPath"
84
+ }
85
+
86
+ # ── 1b. 插件本地运行时 junction:打包代码按真实路径解析 @deepseek-ai/*,
87
+ # 需要插件目录自带 node_modules/@deepseek-ai 指向本机 profile 运行时 ──
88
+ $runtimeScope = Join-Path $dshHome 'profiles\node_modules\@deepseek-ai'
89
+ $pluginScope = Join-Path $pluginRoot 'node_modules\@deepseek-ai'
90
+ if (Test-Path $runtimeScope) {
91
+ if (-not (Test-Path $pluginScope)) {
92
+ New-Item -ItemType Directory -Path (Join-Path $pluginRoot 'node_modules') -Force | Out-Null
93
+ New-Item -ItemType Junction -Path $pluginScope -Target $runtimeScope | Out-Null
94
+ Write-Host "[dsh-win-multi-bash] 已建立插件本地运行时 junction: $pluginScope"
95
+ } elseif ((Get-Item $pluginScope -Force).Target -ine $runtimeScope) {
96
+ Write-Host "[dsh-win-multi-bash] $pluginScope 指向 $((Get-Item $pluginScope -Force).Target),期望 $runtimeScope —— 请手动处理"
97
+ }
98
+ } else {
99
+ Write-Host "[dsh-win-multi-bash] 警告:未找到运行时 $runtimeScope,打包代码的 @deepseek-ai 依赖将无法解析"
100
+ }
101
+
102
+ # ── 2. 本机 Git Bash 探测:默认解析会落空或落到 WSL 启动器时写 bashPath 覆盖 ──
103
+ $gitBashOverride = $null
104
+ $regInstall = $null
105
+ try { $regInstall = (Get-ItemProperty 'HKLM:\SOFTWARE\GitForWindows' -ErrorAction SilentlyContinue).InstallPath } catch { }
106
+ $regBash = if ($regInstall) { Join-Path $regInstall 'usr\bin\bash.exe' } else { $null }
107
+
108
+ # 复刻运行时 candidateBashPaths 的探测顺序(ProgramFiles → ProgramFiles(x86) → PATH)
109
+ $candidates = [System.Collections.Generic.List[string]]::new()
110
+ foreach ($pf in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
111
+ if ($pf) {
112
+ $candidates.Add((Join-Path $pf 'Git\bin\bash.exe'))
113
+ $candidates.Add((Join-Path $pf 'Git\usr\bin\bash.exe'))
114
+ }
115
+ }
116
+ foreach ($pe in ($env:PATH -split ';')) {
117
+ $t = $pe.Trim().Trim('"')
118
+ if ($t) { $candidates.Add((Join-Path $t 'bash.exe')) }
119
+ }
120
+ $firstExisting = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
121
+ if ($null -ne $firstExisting) {
122
+ $desc = (Get-Item $firstExisting).VersionInfo.FileDescription
123
+ if ($desc -match 'Bash Launcher' -or $firstExisting -ieq 'C:\WINDOWS\system32\bash.exe') {
124
+ Write-Host "[dsh-win-multi-bash] 默认解析会命中 WSL 启动器 ($firstExisting),不是 Git Bash"
125
+ if ($regBash -and (Test-Path $regBash)) { $gitBashOverride = $regBash }
126
+ }
127
+ } elseif ($regBash -and (Test-Path $regBash)) {
128
+ Write-Host '[dsh-win-multi-bash] 默认探测路径未找到 Git Bash,使用注册表安装路径'
129
+ $gitBashOverride = $regBash
130
+ }
131
+ if ($gitBashOverride) { Write-Host "[dsh-win-multi-bash] gitBash.bashPath 覆盖 → $gitBashOverride" }
132
+
133
+ # ── 3. 生成 managed 块(统一形态) ───────────────────────────────────────────
134
+ $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
135
+ $gitBashLines = if ($gitBashOverride) {
136
+ " config:`n gitBash:`n bashPath: '$gitBashOverride'"
137
+ } else { '' }
138
+
139
+ $block = @"
140
+ # --- dsh-win-multi-bash managed (auto-generated by install.ps1 at $now; do not edit) ---
141
+ # Windows 多 bash 后端(自包含插件):核心行(包行)+ git_bash / wsl_bash 两个工具行;
142
+ # 共有提示词小节由核心行拥有。每个工具自带执行器,不占用 ctx.shell 席位;
143
+ # 基座自带的 pwsh-sandbox 行继续提供 shell 席位,pwsh 行为不变。
144
+ - insert:
145
+ - id: win-mb-plugin
146
+ name: 'dsh-win-multi-bash'
147
+ disabled: !!js process.platform !== 'win32'
148
+ # 核心行(包自身那一行):name 必须是**裸包名**,Web 端才能解析到本包的
149
+ # package.json 并读到 dsh.client(以下两行的 name 都是子路径,承载不了该声明);
150
+ # 它同时拥有唯一的 tool:win-mb-bash 提示词小节(按实际挂载的工具解析文本,
151
+ # 两个工具都关时不注入任何内容)。请保持开启:关掉它工具仍可用、共有指导会
152
+ # 回落进各自描述,但插件面板会失去配置页。
153
+ - id: win-mb-tool-git
154
+ name: 'dsh-win-multi-bash/tool-git-bash'
155
+ disabled: !!js process.platform !== 'win32'
156
+ $gitBashLines
157
+ # 沙箱强化(可选):探针失败时拒绝无沙箱运行,仅 danger-full-access 放行:
158
+ # gitBash: { requireSandbox: true }
159
+ - id: win-mb-tool-wsl
160
+ name: 'dsh-win-multi-bash/tool-wsl-bash'
161
+ disabled: !!js process.platform !== 'win32'
162
+ # wslBash 分区同样可配(可选):
163
+ # wslBash: { wslDistro: 'Ubuntu-24.04', requireSandbox: true }
164
+ # --- end dsh-win-multi-bash managed ---
165
+ "@
166
+
167
+ # ── 4. 幂等写入:删旧块 → 追加新块(UTF-8 无 BOM) ─────────────────────────
168
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
169
+ $existing = if (Test-Path $patchPath) { [System.IO.File]::ReadAllText($patchPath, $utf8NoBom) } else { '' }
170
+ $pattern = '(?s)# --- dsh-win-multi-bash managed.*?# --- end dsh-win-multi-bash managed ---\r?\n?'
171
+ $cleaned = [regex]::Replace($existing, $pattern, '')
172
+ $separator = if ($cleaned -and -not $cleaned.EndsWith("`n")) { "`n" } else { '' }
173
+ $newContent = $cleaned + $separator + $block
174
+ [System.IO.File]::WriteAllText($patchPath, $newContent, $utf8NoBom)
175
+
176
+ Write-Host ''
177
+ Write-Host "[dsh-win-multi-bash] 已写入 $patchPath"
178
+ Write-Host "[dsh-win-multi-bash] dsh web 正在运行时,watchUserPatches 会热重载本块 —— 新会话即可看到 git_bash / wsl_bash 工具(无需重启)。"
179
+ Write-Host "[dsh-win-multi-bash] 卸载:运行 uninstall.ps1(热移除 + 移除插件 junction,无需重启)。"
@@ -245,25 +245,37 @@ function registryBashPaths() {
245
245
  */
246
246
  var GitBashExecutor = class extends LocalBashExecutor {
247
247
  static Config = deriveConfig(LocalBashExecutor.Config, {
248
- bashPath: z.string(),
249
- sandbox: z.union([z.const("auto"), z.const("none")]).default("auto"),
250
- probeTimeoutMs: z.natural().default(1e4),
251
- requireSandbox: z.boolean().default(false)
248
+ bashPath: z.string().volatile(),
249
+ sandbox: z.union([z.const("auto"), z.const("none")]).default("auto").volatile(),
250
+ probeTimeoutMs: z.natural().default(1e4).volatile(),
251
+ requireSandbox: z.boolean().default(false).volatile()
252
252
  });
253
253
  /** Test hook mirroring the sandbox-local internals pattern. */
254
254
  internals = {};
255
- sandboxStance;
256
- probeTimeoutMs;
255
+ /**
256
+ * The row's own knobs are declared `.volatile()`, so the loader hands each
257
+ * one a live reference rather than a snapshot (the same shape the inherited
258
+ * `cwd` / `timeoutMs` fields already have). Every read goes through `.get()`,
259
+ * which is what makes a value saved on the Plugins page apply to the next
260
+ * command without reloading the row.
261
+ */
262
+ get sandboxStance() {
263
+ return this.config.sandbox.get();
264
+ }
265
+ get probeTimeoutMs() {
266
+ return this.config.probeTimeoutMs.get();
267
+ }
268
+ get requireSandbox() {
269
+ return this.config.requireSandbox.get();
270
+ }
257
271
  /** Memoized probe promise; the provider's `confine` is async as of 0.1.7. */
258
272
  confinedProbe;
259
273
  /** The verdict `resolveSandboxMode` settled on, read back by the sync getter. */
260
274
  sandboxModeVerdict;
275
+ /** The knob values that verdict was settled for, so a live stance edit re-settles. */
276
+ sandboxModeVerdictKey;
261
277
  constructor(ctx, config) {
262
278
  super(ctx, config);
263
- const entry = config;
264
- this.sandboxStance = entry.sandbox;
265
- this.probeTimeoutMs = entry.probeTimeoutMs;
266
- this.requireSandbox = entry.requireSandbox;
267
279
  }
268
280
  /**
269
281
  * The capability fact: the policy default mode while confinement is usable.
@@ -282,7 +294,11 @@ var GitBashExecutor = class extends LocalBashExecutor {
282
294
  * @returns the mode to advertise, or `undefined` for an unconfining deployment.
283
295
  */
284
296
  async resolveSandboxMode() {
285
- if (this.sandboxStance === "none") return this.sandboxModeVerdict = void 0;
297
+ const stance = this.sandboxStance;
298
+ const verdictKey = `${stance}|${String(this.requireSandbox)}`;
299
+ if (this.sandboxModeVerdictKey === verdictKey) return this.sandboxModeVerdict;
300
+ this.sandboxModeVerdictKey = verdictKey;
301
+ if (stance === "none") return this.sandboxModeVerdict = void 0;
286
302
  if (this.requireSandbox) return this.sandboxModeVerdict = this.ctx.sandboxPolicy.defaultMode;
287
303
  return this.sandboxModeVerdict = await this.probeConfinedOnce() ? this.ctx.sandboxPolicy.defaultMode : void 0;
288
304
  }
@@ -296,7 +312,7 @@ var GitBashExecutor = class extends LocalBashExecutor {
296
312
  }
297
313
  /** The resolved Git Bash executable; undefined resolves to a loud route-time error. */
298
314
  tryBashPath() {
299
- const path = (this.internals.resolveBashPath ?? ((configured) => resolveBashPath(configured)))(this.config.bashPath);
315
+ const path = (this.internals.resolveBashPath ?? ((configured) => resolveBashPath(configured)))(this.config.bashPath.get());
300
316
  if (path !== void 0) return path;
301
317
  return (this.internals.registryBashPaths ?? registryBashPaths)();
302
318
  }
@@ -130,29 +130,39 @@ function toWslPath(winPath) {
130
130
  */
131
131
  var WslBashExecutor = class extends LocalBashExecutor {
132
132
  static Config = deriveConfig(LocalBashExecutor.Config, {
133
- wslPath: z.string(),
134
- wslDistro: z.string(),
133
+ wslPath: z.string().volatile(),
134
+ wslDistro: z.string().volatile(),
135
135
  sandbox: z.union([
136
136
  z.const("auto"),
137
137
  z.const("none"),
138
138
  z.const("bwrap")
139
- ]).default("auto"),
140
- probeTimeoutMs: z.natural().default(3e4),
141
- requireSandbox: z.boolean().default(false)
139
+ ]).default("auto").volatile(),
140
+ probeTimeoutMs: z.natural().default(3e4).volatile(),
141
+ requireSandbox: z.boolean().default(false).volatile()
142
142
  });
143
143
  /** Test hook mirroring the sandbox-local internals pattern. */
144
144
  internals = {};
145
- sandboxStance;
146
- probeTimeoutMs;
145
+ /**
146
+ * The row's own knobs are declared `.volatile()`, so the loader hands each
147
+ * one a live reference rather than a snapshot (the same shape the inherited
148
+ * `cwd` / `timeoutMs` fields already have). Every read goes through `.get()`,
149
+ * which is what makes a value saved on the Plugins page apply to the next
150
+ * command without reloading the row.
151
+ */
152
+ get sandboxStance() {
153
+ return this.config.sandbox.get();
154
+ }
155
+ get probeTimeoutMs() {
156
+ return this.config.probeTimeoutMs.get();
157
+ }
158
+ get requireSandbox() {
159
+ return this.config.requireSandbox.get();
160
+ }
147
161
  bwrapVerdict;
148
162
  distroProbed = false;
149
163
  distroVerdict;
150
164
  constructor(ctx, config) {
151
165
  super(ctx, config);
152
- const entry = config;
153
- this.sandboxStance = entry.sandbox;
154
- this.probeTimeoutMs = entry.probeTimeoutMs;
155
- this.requireSandbox = entry.requireSandbox;
156
166
  }
157
167
  /**
158
168
  * The capability fact: the policy default mode while bwrap is usable. With
@@ -182,7 +192,7 @@ var WslBashExecutor = class extends LocalBashExecutor {
182
192
  return false;
183
193
  }
184
194
  tryWslPath() {
185
- return (this.internals.resolveWslPath ?? ((configured) => resolveWslPath(configured)))(this.config.wslPath);
195
+ return (this.internals.resolveWslPath ?? ((configured) => resolveWslPath(configured)))(this.config.wslPath.get());
186
196
  }
187
197
  wslPath() {
188
198
  const path = this.tryWslPath();
@@ -209,7 +219,7 @@ var WslBashExecutor = class extends LocalBashExecutor {
209
219
  return this.distroVerdict;
210
220
  }
211
221
  distro() {
212
- return this.config.wslDistro ?? this.probeDistroOnce();
222
+ return this.config.wslDistro.get() ?? this.probeDistroOnce();
213
223
  }
214
224
  probeBwrapOnce() {
215
225
  if (this.bwrapVerdict !== void 0) return this.bwrapVerdict;