@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.8

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.
Files changed (95) hide show
  1. package/CHANGELOG.en.md +51 -1
  2. package/CHANGELOG.md +51 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +69 -4
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/compat-DR2LvmYZ.js +600 -0
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-Nlp2AiRX.js} +56 -24
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-CVAcT_gy.js +743 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +57 -9
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/compat.d.ts +15 -5
  64. package/lib/types/contract.d.ts +31 -19
  65. package/lib/types/dialects.d.ts +99 -3
  66. package/lib/types/fs.d.ts +32 -10
  67. package/lib/types/index.d.ts +8 -3
  68. package/lib/types/process-fence.d.ts +54 -0
  69. package/lib/types/profile-isolation.d.ts +178 -0
  70. package/lib/types/providers.d.ts +70 -0
  71. package/lib/types/registry.d.ts +132 -31
  72. package/lib/types/roots.d.ts +100 -13
  73. package/lib/types/sandbox.d.ts +59 -3
  74. package/lib/types/scope.d.ts +200 -14
  75. package/lib/types/windows-broker.d.ts +17 -0
  76. package/lib/types/windows-deny.d.ts +1 -0
  77. package/lib/types/windows-profile.d.ts +10 -13
  78. package/lib/types/windows-runner-contract.d.ts +97 -0
  79. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  80. package/lib/windows-runner.js +1256 -13
  81. package/lib/workspace-files.js +138 -3
  82. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  83. package/package.json +84 -62
  84. package/scripts/windows-broker/install.ps1 +89 -0
  85. package/scripts/windows-broker/rights.cs +58 -0
  86. package/scripts/windows-broker/service.cs +323 -0
  87. package/scripts/windows-broker/verify-install.ps1 +80 -0
  88. package/lib/command-CTdNvwt1.js +0 -791
  89. package/lib/compat-B6ZqbQBy.js +0 -295
  90. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  91. package/lib/sandbox-BRXl_FpF.js +0 -132
  92. package/lib/types/client/RootFileTree.d.ts +0 -12
  93. package/lib/types/panel-files.d.ts +0 -3
  94. package/lib/windows-profile-CiFsFYIV.js +0 -216
  95. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
package/CHANGELOG.en.md CHANGED
@@ -8,6 +8,54 @@ This file records the user-visible changes of every **released** version.
8
8
  - For installation sources, the supported upstream runtimes, and known limitations, see the [README](./README.en.md).
9
9
  - The evidence, numbering, and release state of each version live in the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账). The body of each GitHub Release is generated from the commit history by `scripts/release-notes.mjs` and is the complete raw list behind this file.
10
10
 
11
+ ## [0.1.8] - 2026-10-18
12
+
13
+ ### Fixed
14
+
15
+ - **Permission settings page unavailable on DSH 0.2.0**: when installed via `link:`, the plugin resolved optional packages it never loads — packages consumed only as mounted host services (`dsh-lsp` / `dsh-lsp-stdio` / `dsh-working-directory`, among others) — to the checkout's own devDependencies, so a uniformly-versioned host was misjudged as `mixed`; and because the two replacement provider rows injected the compatibility gate while upstream `fs-sandbox` / `sandbox` were already disabled unconditionally, a refused gate removed `ctx.fs` / `ctx.sandbox`, taking down `ctx.shell` and `permissionPresets` with it (`permissionPresets/catalog` reported `service-unavailable`). Optional packages are now classified by who loads the resolved copy and through which anchor — runtime-loaded, host-service, or runner-anchored — so host-service copies inside the plugin root no longer testify, and the Windows ACL package is inspected from the `dsh-sandbox-local` anchor; the two provider rows no longer inject the gate and degrade to upstream-equivalent behavior on a refused installation, while every widened-authority row stays gated (see [ADR-0009 amendment](./docs/decisions/ADR-0009-dsh-compat-contract.md#修订2026-10-18permissionpresetscatalog-事故误报检测--被门禁的-provider)).
16
+
17
+ ## [0.1.7] - 2026-10-17
18
+
19
+ ### Added
20
+
21
+ - **Browse additional roots inside the Files tab**: a root switcher sits in the file-tree header (a menu entry disguised as the path label, listing the primary root plus every additional root and its state); picking an additional root hands the tree area to a byte-for-byte fork of the upstream Files tree — row height, icons, indent, and the empty state match natively item by item, the native reload is replaced by an identical icon button at the header's original spot, and switching back to the primary root restores everything. The native open-in-app button is replaced by a fork that retargets to the selected root ([ADR-0015](./docs/decisions/ADR-0015-files-tab-plugin-owned-browsing.md), [ADR-0016](./docs/decisions/ADR-0016-files-tab-native-tree-fork.md)). Panel rows carry an access badge, every path copies through the same `PathCopy` control in one tap, read-only → read-write promotion goes through a confirmation layer, and the add-row access picker is an anchored dropdown.
22
+ - **Per-root access levels are now three-state**: `add [--inherit|--read-only|--read-write]` (or the panel's add-row selector). `inherit` follows the session mode — it is the default for new registrations, and writable records stored by older plugin versions migrate to it; `read-only` stays readable, browsable, and part of LSP and instruction delivery but grants no write authority in any mode, including `danger-full-access`; a fixed `read-write` stays writable even in a globally read-only session. `/workspace-folders access <ref> inherit|read-only|read-write` and the panel switch access per root (widening requires the root to be available and never re-authorizes its path). The persistence layout stays downgrade-safe: an older plugin reads a read-only record as invalid rather than upgrading it to writable ([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md), [ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
23
+ - **Full-access now enforces deny boundaries too**: the shell / pwsh executors, `run_code` (PTC), and agent PTYs all bypass `confine` in `danger-full-access`, so a process fence puts them under a restricted kernel profile (macOS Seatbelt, Linux bwrap — a host where upstream selected Landlock uses bubblewrap the same way, and refuses the combination at launch when bwrap is unusable); explicit read-only roots and tool-capped members stay denied in that mode, and the fs fence reads the same deny set ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
24
+ - **A restricted Full-access also protects `$DSH_HOME`**: whenever any deny boundary exists (a read-only root, a tool-capped member, or an unavailable registry authority), the home directory joins the deny set — the durable store, the registry lease, and user profiles live there; with zero additional roots and a healthy authority Full-access stays byte-identical to upstream ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
25
+ - **Your panel terminals are never confined**: the process fence only prefixes spawns whose env carries `DSH_PTY_SESSION_ID` (stamped by the terminal-bash backend for agent PTYs); a terminal you open from the panel is user behavior and passes through in every mode ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
26
+ - **Selective read-only under Windows Full-access**: an opt-in, administrator-installed per-user privileged broker ships with the package — the installer grants a fixed set of LSA rights (`SeServiceLogonRight`, `SeCreateTokenPrivilege`, `SeAssignPrimaryTokenPrivilege`, `SeTcbPrivilege`, `SeDebugPrivilege`, `SeImpersonatePrivilege`) to the dedicated virtual service account `NT SERVICE\DshMultiRootBroker-<install hash>` only and never elevates the host user; the broker builds a normal deny-only group token for the restricted child, which the runner reads back and verifies before letting it run. Without the broker the combination refuses to launch; Windows enforcement still honestly reports its partial boundary ([ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md)).
27
+ - **Per-tool write caps**: configuring `toolAccess: { <tool>: 'read-only' }` on the `multi-root-scope` row makes that tool permanently unable to write any additional root (the primary root is unaffected); enforcement rides the real `tools/execute` dispatch context, and invalid configuration fails closed ([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md)).
28
+ - **Additional-root limit**: each primary root stores at most 16 additional-root records by default (`maxAdditionalRoots`, up to 64); new additions past the limit are refused while over-limit history keeps granting. `list` prints `N of LIMIT` honestly ([ADR-0014](./docs/decisions/ADR-0014-root-limit-and-provider-conflict.md)).
29
+ - **Provider-conflict detection**: when another plugin also replaces `ctx.fs` / `ctx.sandbox`, the multi-root rows fail loudly and grant nothing instead of depending on load order ([ADR-0014](./docs/decisions/ADR-0014-root-limit-and-provider-conflict.md)).
30
+ - **Exact support for DSH `0.2.1-alpha.3`, `0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, and `0.2.1-rc.2`**. Since `alpha.3`, upstream `dsh-storage-domain` pins `zod` at `4.4.3`, and the two resolved zod copies' types are mutually incompatible, so the domain schemas are now built by the zod the storage domain itself resolves. Since `alpha.4`, upstream version requirements prefer `engines.dsh`; this plugin still keeps `peerDependencies` as its single version contract. The dev pin moves to `0.2.1-rc.2` (cordis `4.0.5`; `alpha.3`–`alpha.5` use cordis `4.0.5-alpha.2`).
31
+
32
+ ### Changed
33
+
34
+ - **Model-facing topology rewritten**: the prompt states authority truthfully per mode and platform — writable and read-only roots listed separately, cd/worktree switches do not change grants, grant changes constrain only later operations and new processes, capped tools are named, and Windows adds the restricted-token partial-enforcement caveat plus the note that reads are not confined by it ([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md)).
35
+
36
+ ### Fixed
37
+
38
+ - **An unavailable registry authority no longer reads as "no restrictions"**: a process that read an empty root table (contended, storage failure, invalid configuration) used to let Full-access write anywhere; `danger-full-access` and `workspace-write` now grant writes only to the primary root (the shared platform temp areas are no longer writable), explicit deny boundaries still win, and the real scope returns once the lease is taken over ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
39
+ - **Renaming an ancestor can no longer lift a read-only boundary**: renaming a denied root's parent used to move the tree outside its path-based restriction. Seatbelt now also denies writes to each ancestor directory itself (`(literal …)`), and bwrap self-binds ancestors into mountpoints so rename fails with EBUSY. Windows pins the root and its ancestors with directory handles that disallow delete sharing until the restricted Job's descendants have drained, preventing a shell rename followed by a host file-tool bypass. These mechanisms constrain ancestor renames or metadata operations ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md), [ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md)).
40
+ - **Negative authorization survives reordering, disappearance, and conflicts**: overlapping stored registrations both lose membership grants while retaining their trusted deny boundaries. New registrations also check the reserved boundaries of missing or redirected roots. Globally read-only sessions apply denies after adding fixed read-write roots; backends that cannot express subtree subtraction refuse to launch.
41
+ - **Windows checks every deny root for hardlinks before ACL changes**: any multi-link file or uninspectable node refuses startup before any ACL write, avoiding changes to shared objects in the pnpm store or other link locations. The host maintenance command `/workspace-folders isolate-profiles [--apply]` inspects by default; explicit apply only sets profile imports to copy. Existing dependencies still require the reported reinstall steps, and historical ACLs are not repaired ([ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md)).
42
+ - **A deny root that vanished on Linux fails closed instead of opening up**: the root's bwrap mount is mandatory again — when the directory disappears after registration or after the profile is built, namespace setup fails and the command never runs, instead of the path becoming recreatable under a writable parent mount ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
43
+ - **Re-registering roots preserves their access**: the command and panel omit access unless explicitly chosen, so reviving a read-only registration cannot silently upgrade it; the panel keeps re-registration available for existing unavailable entries at the root limit.
44
+ - **The panel accepts older hosts' root lists**: responses without `limit` / `count` remain usable, while explicitly malformed values are still rejected.
45
+ - **Invalid toolAccess is reflected in the topology prompt**: the model sees that all additional-root writes are refused instead of being told read-write registrations are writable; a `null` root-limit configuration also fails closed.
46
+ - **Confined shell commands work again for installed copies on Windows (DSH 0.1.7-alpha.1+)**: with the published v0.1.6 installed from npm/tarball, the standalone Windows runner died before its first line with `ERR_MODULE_NOT_FOUND` — its static module graph imported upstream packages that cannot be resolved from the plugin's install directory, so every confined command in a session with additional roots failed on the affected releases. The runner's static graph is now `node:` builtins only, and it loads the upstream ACL package by self-reference from the upstream runner's own location.
47
+ - **Network and device roots are rejected on Windows**: UNC (`\\server\share`), `\\?\`, `\\.\`, and mapped network drives were previously accepted as ordinary additional roots; they are now rejected lexically as `unsupported-path` before any filesystem access, stored records are marked `invalid`, and the confined-execution side fails closed (`SandboxUnavailableError` / exit 127) rather than silently running unconfined ([ADR-0011 amendment](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
48
+ - **The Windows runner re-verifies roots before spawn**: after the ACL grant and before the child starts, every root must still exist and resolve to the same canonical directory, and `--temp` must be an existing directory; anything else exits 127 without launching an unconfined process.
49
+ - **"Reveal in file manager" no longer reports failure on Windows**: `explorer.exe` returns 1 on success, so exit codes 0 and 1 are both accepted on win32.
50
+ - **Panel file endpoints reject backslash paths**: on POSIX, `..\..\x` previously resolved inside the selected root; backslashes are now rejected lexically.
51
+ - **The Windows broker accepts UAC-filtered callers and diagnoses packaged children**: restricted commands launched by a non-elevated UAC administrator were previously refused for not being an "unrestricted identity"; only callers carrying restricting SIDs or an AppContainer are refused now, and a Store/MSIX child (such as `pwsh.exe` under the WindowsApps alias) gets a diagnostic pointing at the MSI/ZIP build before launch. After upgrading, reinstall the broker once with `install.ps1` for this to take effect ([ADR-0020 amendment](./docs/decisions/ADR-0020-windows-normal-deny-groups.md)).
52
+ - **Panel file-tree calls are abortable**: in-flight `files` / `readFile` calls are cancelled on unmount, path, or session change, so stale responses no longer render as errors.
53
+
54
+ ### Changed
55
+
56
+ - **Confined Windows shell behavior, documented**: the confined shell is pwsh (Git Bash/MSYS cannot start under the restricted token, identically to the upstream runner); under the plugin runner a confined child receives Ctrl+C and the runner mirrors its exit code, while an upstream-confined child inherits the runner's ignore-Ctrl+C flag and cannot receive it — both are established behavior ([ADR-0011 amendment](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
57
+ - Install note added: if the first install was interrupted by the `koffi` build approval, a successful follow-up `add` may still leave the plugin absent from `dsh.profile.bundles`; one `remove` + `add` activates it ([troubleshooting](./docs/troubleshooting/install-stops-at-build-approval.md)).
58
+
11
59
  ## [0.1.6] - 2026-10-11
12
60
 
13
61
  ### Added
@@ -114,7 +162,9 @@ The first release: the three MVP milestones (M1–M3).
114
162
 
115
163
  - All eight findings of the pre-release external review (three of them release-blocking) are fixed, each with a regression test: a registered directory replaced by a symlink transplanting its authority (which introduced `recordedPath` — **re-resolving a path is not re-authorizing it**), concurrent registry mutations losing a write or reviving a revoked grant (per-primary-root mutations are serialized), a refresh not re-checking directories, a typed path being overwritten by the directory picker, the `reveal` response contract and failure code, duplicate ids not validated, the primary root missing from nesting validation, and CI testing before building.
116
164
 
117
- [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.6...HEAD
165
+ [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.8...HEAD
166
+ [0.1.8]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.7...v0.1.8
167
+ [0.1.7]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.6...v0.1.7
118
168
  [0.1.6]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.5...v0.1.6
119
169
  [0.1.5]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.4...v0.1.5
120
170
  [0.1.4]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.3...v0.1.4
package/CHANGELOG.md CHANGED
@@ -8,6 +8,54 @@ English: [CHANGELOG.en.md](./CHANGELOG.en.md)
8
8
  - 安装方式、受支持的上游运行时与已知限制见 [README](./README.md)。
9
9
  - 每个版本的实施证据、编号口径与发布状态见[路线图 §进度总账](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账);GitHub Release 的正文由 `scripts/release-notes.mjs` 从 commit 历史生成,是本文件之外的完整原始清单。
10
10
 
11
+ ## [0.1.8] - 2026-10-18
12
+
13
+ ### Fixed
14
+
15
+ - **DSH 0.2.0 上权限设置页不可用**:以 `link:` 方式安装时,插件把只作为宿主服务消费、自身从不加载的可选包(`dsh-lsp` / `dsh-lsp-stdio` / `dsh-working-directory` 等)解析到检出目录自己的 devDependencies,版本统一的宿主被误判为 `mixed`;同时两个 provider 替身行 inject 兼容门禁而上游 `fs-sandbox` / `sandbox` 已被无条件禁用,门禁一旦拒绝,`ctx.fs` / `ctx.sandbox` 消失并连带 `ctx.shell` 与 `permissionPresets` 瘫痪(`permissionPresets/catalog` 报 `service-unavailable`)。可选包现按"谁加载副本、经哪个锚点"分为运行时加载、宿主服务与 runner 锚定三类——插件根内的宿主服务副本不再作证,Windows ACL 包改从 `dsh-sandbox-local` 锚点取证;两个 provider 行不再 inject 门禁,在被拒安装上退化为上游等价行为,宽授权行的门禁不变([ADR-0009 修订](./docs/decisions/ADR-0009-dsh-compat-contract.md#修订2026-10-18permissionpresetscatalog-事故误报检测--被门禁的-provider))。
16
+
17
+ ## [0.1.7] - 2026-10-17
18
+
19
+ ### Added
20
+
21
+ - **Files 页签内浏览附加根**:文件树 header 新增根切换器(伪装成路径标签的菜单入口,列出主根与全部附加根及其状态);选中附加根时由逐字节复刻的上游 Files 树接管树区——行高、图标、缩进、空态与原生逐项一致,原生 reload 由 header 原位同款图标钮顶替,切回主根即完整还原。原生 open-in-app 按钮由复刻件顶替并把目标换成选中根([ADR-0015](./docs/decisions/ADR-0015-files-tab-plugin-owned-browsing.md)、[ADR-0016](./docs/decisions/ADR-0016-files-tab-native-tree-fork.md))。面板行带访问级别徽标,所有路径用同一 `PathCopy` 控件一点即复制;只读 → 读写升级经确认层;添加行的权限选择器为锚定下拉。
22
+ - **附加根访问级别升级为三态**:`add [--inherit|--read-only|--read-write]`(或面板添加行选择)。`inherit` 跟随会话模式——新登记的默认值,旧版插件登记的可写记录也迁移为 `inherit`;`read-only` 可读、可浏览、参与 LSP 与指令注入,但任何模式(包括 `danger-full-access`)都不授予写权限;`read-write` 固定在全局只读会话里仍可写。`/workspace-folders access <ref> inherit|read-only|read-write` 与面板按根切换(扩权要求该根此刻可用且不重新授权路径)。持久化布局向下兼容:旧版本插件把只读记录判为无效而非升级为可写([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md)、[ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
23
+ - **Full-access 也强制执行拒写边界**:shell / pwsh 执行器、`run_code`(PTC)与 agent PTY 在 `danger-full-access` 下绕过 `confine`,进程围栏为它们统一套用受限内核 profile(macOS Seatbelt、Linux bwrap——上游选定 Landlock 的主机同样改走 bubblewrap,bwrap 不可用时该组合在启动前拒绝),显式只读根与被工具封顶的根在该模式下依旧拒写;fs fence 读取同一份 deny 集([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
24
+ - **受限 Full-access 保护 `$DSH_HOME`**:只要存在任何拒写边界(只读根、工具封顶成员或注册表权限不可用),home 目录就进入 deny 集——存根存储、注册表 lease 与用户 profile 都在其中;零附加根且注册表健康时 Full-access 与上游逐字节一致([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
25
+ - **面板用户终端不受约束**:进程围栏只对 env 带 `DSH_PTY_SESSION_ID` 的 spawn 生效(agent PTY 由 terminal-bash backend 盖章);你在面板打开的终端属于用户行为,任何模式直通([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
26
+ - **Windows 上 Full-access 的局部拒写**:随包提供需管理员安装的每用户特权 broker——安装器只向专用虚拟服务账户 `NT SERVICE\DshMultiRootBroker-<安装哈希>` 授予固定 LSA 权限(`SeServiceLogonRight`、`SeCreateTokenPrivilege`、`SeAssignPrimaryTokenPrivilege`、`SeTcbPrivilege`、`SeDebugPrivilege`、`SeImpersonatePrivilege`),不为宿主用户提权;broker 为受限子进程构造普通 deny-only 组 token,runner 读回验证后才放行。未安装 broker 时该组合拒绝启动;Windows 执行仍如实报告 partial 边界([ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md))。
27
+ - **per-tool 写权限封顶**:`multi-root-scope` 行配置 `toolAccess: { <工具名>: 'read-only' }` 后,该工具永远不能写任何附加根(主根不受影响);封顶经 `tools/execute` 的真实 dispatch 上下文执行,非法配置 fail closed([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md))。
28
+ - **附加根数量上限**:每个主根默认最多登记 16 条附加根记录(`maxAdditionalRoots` 可调至 64);超限的新增被拒绝,历史超限记录照常授予。列表如实显示 `N of LIMIT`([ADR-0014](./docs/decisions/ADR-0014-root-limit-and-provider-conflict.md))。
29
+ - **Provider 冲突检测**:另一插件也替换 `ctx.fs` / `ctx.sandbox` 时,多根行响亮失败并拒绝一切授予,而不是依赖装载顺序([ADR-0014](./docs/decisions/ADR-0014-root-limit-and-provider-conflict.md))。
30
+ - **精确支持 DSH `0.2.1-alpha.3`、`0.2.1-alpha.4`、`0.2.1-alpha.5`、`0.2.1-rc.1` 与 `0.2.1-rc.2`**。上游 `alpha.3` 起 `dsh-storage-domain` 把 `zod` 固定为 `4.4.3`,两个 zod 副本的类型互不兼容,域 schema 改由存储域自身解析的 zod 构建;`alpha.4` 起上游版本需求优先读 `engines.dsh`,本插件仍以 `peerDependencies` 为唯一版本契约。开发 pin 提升到 `0.2.1-rc.2`(cordis `4.0.5`;`alpha.3`–`alpha.5` 的 cordis 为 `4.0.5-alpha.2`)。
31
+
32
+ ### Changed
33
+
34
+ - **模型侧拓扑文案重写**:按当前模式与平台如实陈述授权——可写与只读根分组列出,声明 cwd/worktree 切换不改变授权、授权变化只约束后续操作与新进程;配置封顶工具时点名;Windows 附带 restricted-token partial-enforcement 与读不受其 confine 的说明([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md))。
35
+
36
+ ### Fixed
37
+
38
+ - **注册表权限不可用不再读作「没有限制」**:读到空根表的进程(争用、存储故障、配置非法)曾在 Full-access 下放行一切写;现在 `danger-full-access` 与 `workspace-write` 都只授予主根写权限(共享平台临时区不再可写),显式拒写边界仍优先,接管 lease 后恢复([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
39
+ - **改名祖先目录不能再解除只读边界**:此前把被拒根的父目录改名即可把整棵树搬出路径拒写边界;现在 Seatbelt 同时拒写被拒根的每个祖先目录本身(`(literal …)`),bwrap 把祖先自绑为挂接点使 rename 返回 EBUSY;Windows 用不共享删除的目录句柄钉住根及祖先,直到受限 Job 后代排空,防止 shell 搬走目录后经宿主文件工具绕过。祖先目录的元数据或改名操作会受相应机制限制([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)、[ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md))。
40
+ - **负授权不随排序、消失或冲突丢失**:存量重叠登记双方撤销成员授权,但可信记录的拒写边界仍保留;新增根也检查 missing / redirected 根的保留边界。全局只读会话追加固定读写根后统一应用 deny,无法表达子树减法的后端在启动前拒绝。
41
+ - **Windows 拒写前检查全部根的硬链接**:任一根存在多链接文件或不可检查节点时,在任何 ACL 写入前拒绝启动,避免改写 pnpm store 或其他链接的共享对象。新增宿主维护命令 `/workspace-folders isolate-profiles [--apply]`:默认只读检查,显式 apply 仅将 profile 导入配置设为 copy;存量依赖仍需按报告重装,不修复历史 ACL([ADR-0020](./docs/decisions/ADR-0020-windows-normal-deny-groups.md))。
42
+ - **Linux 上消失的拒写根失败关闭而非放行**:deny 根的 bwrap 挂载恢复为强制项——目录在登记后或 profile 构造后被删除时命名空间搭建失败、命令不会执行,不再出现在可写父挂载下被重建写入的缺口([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md))。
43
+ - **附加根重新登记保留原权限**:命令与面板在未显式选择访问级别时不发送默认值,避免 revive 把只读登记静默升级;达到根上限时仍可从面板重新登记既有的不可用项。
44
+ - **面板兼容旧宿主的根列表**:旧响应缺少 `limit` / `count` 时仍可显示和操作,显式非法值仍拒绝。
45
+ - **非法 toolAccess 的拓扑文案与执行一致**:模型明确看到所有附加根写被拒,不再被告知 read-write 登记可写;根上限配置 `null` 同样 fail closed。
46
+ - **安装副本在 Windows(DSH 0.1.7-alpha.1+)上的受限命令恢复可用**:发布的 v0.1.6 从 npm/tarball 安装后,独立 Windows runner 在首行之前就死于 `ERR_MODULE_NOT_FOUND`——它的静态模块图 import 的上游包无法从插件安装目录解析,受影响版本上配置了附加根的会话里每条受限命令都会失败。runner 的静态图现在只含 `node:` 内置,上游 ACL 包改为从上游 runner 自身位置按包自引用加载。
47
+ - **Windows 上的网络与设备根被拒绝**:UNC(`\\server\share`)、`\\?\`、`\\.\` 与映射网络盘此前会被当成普通附加根登记;现在在任何文件系统触碰之前以 `unsupported-path` 词法拒绝,存量记录标记为 `invalid`,受限执行侧 fail closed(`SandboxUnavailableError` / 退出 127),绝不静默放开([ADR-0011 修订](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md))。
48
+ - **Windows runner 在 spawn 前重新验证根**:授权 ACL 之后、启动子进程之前重新确认每个根仍存在且是同一 canonical 目录,并要求 `--temp` 已存在;不满足即退出 127,不启动无约束进程。
49
+ - **Windows 上「在文件管理器中显示」不再误报失败**:`explorer.exe` 成功时也返回 1,win32 现在接受退出码 0/1。
50
+ - **面板文件端点拒绝反斜杠路径**:POSIX 上 `..\..\x` 此前会解析到所选根内部;现在词法拒绝。
51
+ - **Windows broker 放行 UAC 过滤调用方并诊断打包子进程**:非提权 UAC 管理员发起的受限命令此前被 broker 以「unrestricted 身份」拒绝;现在只拒绝携带 restricting SID 或 AppContainer 的调用方,Store/MSIX 版子进程(如 WindowsApps 别名的 `pwsh.exe`)在启动前给出指向 MSI/ZIP 版的诊断。升级后需用 `install.ps1` 重装一次 broker 才会生效([ADR-0020 修订](./docs/decisions/ADR-0020-windows-normal-deny-groups.md))。
52
+ - **面板文件树调用可中止**:组件卸载、路径或会话变化时取消在飞的 `files` / `readFile` 调用,过期响应不再渲染为错误。
53
+
54
+ ### Changed
55
+
56
+ - **Windows 受限 shell 行为说明**:受限 shell 是 pwsh(Git Bash/MSYS 在 restricted token 下无法启动,与上游 runner 一致);在插件 runner 下 confined 子进程能收到 Ctrl+C 且 runner 镜像其退出码,而上游 runner 的子进程因继承 ignore-Ctrl+C 标志收不到(两者均为既成事实,见 [ADR-0011 修订](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md))。
57
+ - 安装提示补充:被 `koffi` 构建授权打断的首次安装,在授权后若插件未进 `dsh.profile.bundles`,需一次 `remove` + `add` 完成激活([故障排查](./docs/troubleshooting/install-stops-at-build-approval.md))。
58
+
11
59
  ## [0.1.6] - 2026-10-11
12
60
 
13
61
  ### Added
@@ -114,7 +162,9 @@ English: [CHANGELOG.en.md](./CHANGELOG.en.md)
114
162
 
115
163
  - 发布前外部评审提出的 8 项发现(其中 3 项发布阻断)全部修复,每项各带一个回归测试:登记目录被替换为符号链接后授权转移(引入 `recordedPath`,**重新解析路径不等于重新授权**)、并发修改注册表丢写或复活已撤销的授权(每个主根的变更串行)、刷新不重新检查目录、手输路径被目录选择器覆盖、`reveal` 的应答契约与失败码、重复 id 未校验、主根未纳入嵌套校验、CI 先测后构建。
116
164
 
117
- [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.6...HEAD
165
+ [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.8...HEAD
166
+ [0.1.8]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.7...v0.1.8
167
+ [0.1.7]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.6...v0.1.7
118
168
  [0.1.6]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.5...v0.1.6
119
169
  [0.1.5]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.4...v0.1.5
120
170
  [0.1.4]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.3...v0.1.4
package/README.en.md CHANGED
@@ -1,59 +1,58 @@
1
+ <div align="center">
2
+
1
3
  # dsh-plugin-multi-root-workspace
2
4
 
3
5
  [中文](./README.md) | English
4
6
 
5
- ## What & Why
6
-
7
- An out-of-tree plugin bundle for DSH (DeepSeek Harness) that widens the Workspace write scope from a single canonical directory to "**one primary root + N additional roots**" — **without modifying any package in the upstream repository**.
8
-
9
- The problem it solves: a real development project is usually several independent Git repositories, while DSH natively treats only the session origin as the writable root, so working across repositories means juggling sessions.
10
-
11
- Three things make this plugin worth looking at:
12
-
13
- - **Zero learning cost for the agent**: it keeps using the native `read` / `write` / `edit` / `bash` tools. The plugin adds no `workspace_*` tools at all; it only widens the answer to the one question "which directories belong to this workspace".
14
- - **Security does not degrade**: the additional roots are granted through the same mechanisms the harness already uses — the in-process fs fence plus kernel-level runners (Seatbelt on macOS, bwrap / Landlock on Linux) — with the fs side and bash/PTY sharing one and the same scope. It never degenerates into danger-full-access or prompt-level constraints.
15
- - **Absent when absent**: it replaces the upstream `fs-sandbox` and `sandbox` provider rows through a bundle patch; with no additional root configured its behavior is item-for-item identical to an uninstalled harness, and every misconfiguration fails loudly instead of silently degrading.
7
+ </div>
16
8
 
17
- **Done and released: the MVP `v0.1.0` (milestones M1–M3)** — composition and empty-root pass-through, multi-root capability and dialect grants, the root registry / the `/workspace-folders` command / the Workspace Folders panel / a cross-repository journey e2e.
18
-
19
- **Done and released: the `v0.1.1` hardening batch (H1–H4)** — a cross-process Registry Authority Lease (when two DSH processes share one `$DSH_HOME`, only the lease holder grants additional roots; the other fails closed and takes over once the holder exits), panel primary-root authority derived from the host session (a client-named root is no longer accepted), DSH compatibility turned from a documented agreement into a **code contract enforced at startup** (an exact allowlist, mixed-install detection, a `src/compat/` adapter layer — see the "Upgrade Notes" in the [CHANGELOG](./CHANGELOG.en.md)), and **an additional root's own `AGENTS.md` / `CLAUDE.md` reaching the model** — native instruction discovery walks upward from the session cwd, so it can never reach an additional root.
20
-
21
- **Done and released: `v0.1.2`** — with the `v0.1.1` contract unchanged, the supported upstream runtimes grow to `0.1.6-alpha.2` and `0.1.7-alpha.1` (the latter needs adapters for session format 4, in-process `PluginPackages`, bash `execute().result()`, and more); it also fixes the Workspace Folders panel on `0.1.6-alpha.2`, where dropping `current` from the Session catalog wrongly showed "No active session". See the [CHANGELOG](./CHANGELOG.en.md).
9
+ ## What & Why
22
10
 
23
- **Done and released: `v0.1.3`** — with the `v0.1.2` contract unchanged, the supported upstream runtimes grow to `0.1.7-alpha.2` and `0.1.7-rc.1` (same shapes as `0.1.7-alpha.1`, no new adapter branch; from `rc.1`, install time enforces an exact `peerDependencies` match against the host runtime). See the [CHANGELOG](./CHANGELOG.en.md).
11
+ DSH (DeepSeek Harness) natively treats only the directory a session starts in as the agent's sole writable root; this plugin is an out-of-tree extension that modifies no upstream code and widens that scope to "**one primary root + N additional roots**".
24
12
 
25
- **Done and released: `v0.1.4`** — with the `v0.1.3` contract unchanged, the supported upstream runtimes grow to `0.1.7-rc.2` (same shapes as `0.1.7-rc.1`, no new adapter branch; cordis stays at `~4.0.4`). See the [CHANGELOG](./CHANGELOG.en.md).
13
+ A real development project is usually several independent Git repositories, and working across them meant juggling sessions. Peer harnesses already treat this as standard: Codex offers `--add-dir`, and Cursor lets you add folders to a workspace. This plugin brings the same capability to DSH — register the other repositories as additional roots, and the agent can read and write them directly.
26
14
 
27
- **`v0.1.5` is released**. This development branch contains unreleased support for DSH `0.2.1-alpha.1` / `0.2.1-alpha.2`, separation of dynamic cwd from Session origin, and LSP, Windows ACL, additional-root file browsing, and Worktree capabilities. These changes are proposed for `v0.1.6`; see the [roadmap ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md) for release status.
15
+ - **Zero learning cost for the agent**: it keeps using the native `read` / `write` / `edit` / `bash` tools — the plugin adds no new tools at all, it only grants more directories.
16
+ - **Security does not degrade**: additional roots are authorized through the same mechanism the harness already uses — an in-process filesystem fence plus kernel-level sandboxes (macOS Seatbelt / Linux bwrap / Landlock / Windows ACL), with files and bash sharing one and the same scope. It never degenerates into full access or prompt-level constraints.
17
+ - **Absent when absent**: with no additional root configured, its behavior is item-for-item identical to an uninstalled harness; every misconfiguration fails loudly instead of silently degrading.
28
18
 
29
- The single source of truth for progress, numbering, and release state is the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账) (M1–M4 are the MVP milestone numbers, H1–H4 are the `v0.1.1` batch numbers, and H1 is M4); per-item evidence lives in the [completed plans](./docs/plans/README.md), and the user-visible changes of each version are in the [CHANGELOG](./CHANGELOG.en.md).
19
+ Current released version: `v0.1.8`. The user-visible changes of each version live in the [CHANGELOG](./CHANGELOG.en.md); progress and release state live in the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账).
30
20
 
31
21
  ## Quick Start
32
22
 
33
- With a DSH runtime at hand (web / Electron desktop / headless all work), the shortest path is two commands — the plugin is published on [npm](https://www.npmjs.com/package/@dsh-electron/dsh-plugin-multi-root-workspace):
23
+ The plugin is published on [npm](https://www.npmjs.com/package/@dsh-electron/dsh-plugin-multi-root-workspace). You can paste the package name `@dsh-electron/dsh-plugin-multi-root-workspace` into DSH's plugin install page, or install it in two commands:
34
24
 
35
25
  ```sh
36
26
  dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace
37
27
  dsh --profile web
28
+ # pick the --profile for how you launch DSH; see its docs for the values
38
29
  ```
39
30
 
40
31
  Once it is up, the **Folders** action (`🗂`) appears at the sidebar foot — or, straight in a session:
41
32
 
42
33
  ```text
43
- /workspace-folders add ~/code/another-repo
34
+ /workspace-folders add ~/code/another-repo [--inherit|--read-only|--read-write]
44
35
  ```
45
36
 
46
- The agent can now read, write, and run bash in that directory, with the same rights as this session's workspace. Other install sources (the GitHub repository / a local clone) and the command differences of running DSH from a source checkout are covered under [Installation](#installation).
37
+ The agent can now read, write, and run bash in that directory — the default is `inherit`, so the access level follows the session mode. Other install options (tarball, GitHub, a local clone) are covered under [Installation](#installation).
47
38
 
48
39
  ## Requirements
49
40
 
50
- - **Using a plugin built from this checkout**: you need a *supported* DSH runtime — currently **`0.1.5-rc.2`, `0.1.6-alpha.1`, `0.1.6-alpha.2`, `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2`, `0.2.0-rc.1`, `0.2.0-rc.2`, `0.2.1-alpha.1`, and `0.2.1-alpha.2`**, and nothing else will install or run (see below). `dsh plugin` installs the package into the matching profile, and no local Node toolchain is required
51
- - **Building from source / contributing**: **Node.js** `^22.19.0 || >=24` (pinned by the repository's `engines`), **Git**, and **pnpm 11** (`packageManager` pins `pnpm@11.25.0`; corepack recommended)
52
- - **DSH runtime**: the dev pin is exactly `0.2.1-alpha.2`, the baseline among the supported releases; the upgrade procedure lives in the [development workflow](./docs/development/plugin-development-workflow.md)
53
- - **Platforms**: macOS Seatbelt and Linux bwrap/Landlock; the development branch also implements a Windows root-set ACL runner. Platform validation is recorded in the [roadmap](./docs/plans/active/2026-09-12-multi-root-workspace.md#b1b3第二期-p12026-10-02).
54
- - Running the smoke tests needs **no model credentials**: the e2e model turns are served by an inline scripted model endpoint
41
+ **Using the plugin**: a DSH runtime at exactly one of the versions below — an exact match, not a range. `dsh plugin` handles the install; no local Node toolchain is needed.
42
+
43
+ | DSH line | Supported releases |
44
+ | --- | --- |
45
+ | `0.1.5` | `0.1.5-rc.2` |
46
+ | `0.1.6` | `0.1.6-alpha.1`, `0.1.6-alpha.2` |
47
+ | `0.1.7` | `0.1.7-alpha.1`, `0.1.7-alpha.2`, `0.1.7-rc.1`, `0.1.7-rc.2` |
48
+ | `0.2.0` | `0.2.0-rc.1`, `0.2.0-rc.2` |
49
+ | `0.2.1` | `0.2.1-alpha.1`, `0.2.1-alpha.2`, `0.2.1-alpha.3`, `0.2.1-alpha.4`, `0.2.1-alpha.5`, `0.2.1-rc.1`, `0.2.1-rc.2` |
50
+
51
+ The plugin does not start — as if it were not installed — when the DSH release is not in the table or the `@deepseek-ai/dsh-*` packages disagree on a version.
55
52
 
56
- **The supported DSH versions are an exact list, not a range.** This plugin replaces `ctx.fs` and `ctx.sandbox` — the security boundary itself — and it recognizes kernel sandbox dialects from argv shapes measured against specific upstream releases. So `peerDependencies` names only the releases that have actually been through the full verification, and startup checks again: if the host's release is not on the list, or several `@deepseek-ai/dsh-*` packages disagree about which release they are, the fs / sandbox / registry / instructions / lsp / workspace-files rows **do not start**, the composition degrades to "this plugin is not installed", and one explanatory line is logged. To diagnose, see [troubleshooting: unsupported DSH release](./docs/troubleshooting/unsupported-dsh-release.md); for the reasoning, [ADR-0009](./docs/decisions/ADR-0009-dsh-compat-contract.md).
53
+ **Building from source / contributing**: Node.js `^22.19.0 || >=24`, pnpm `11.25.0` (enable via corepack); the dev-pinned DSH is `0.2.1-rc.2`.
54
+
55
+ **Platforms**: macOS (Seatbelt), Linux (bwrap / Landlock), Windows (root-set ACL runner).
57
56
 
58
57
  ## Installation
59
58
 
@@ -61,19 +60,19 @@ The agent can now read, write, and run bash in that directory, with the same rig
61
60
 
62
61
  | Source | Command | Notes |
63
62
  | --- | --- | --- |
64
- | npm registry (recommended) | `dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace` | Pre-built artifacts, ready to use; the first `add` needs one `allowBuilds` answer (below) |
65
- | tarball | `dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-<version>.tgz` | Pre-built offline package, handy for air-gapped or offline delivery — with the same one-time `allowBuilds` answer |
66
- | local path | `dsh plugin --profile web add /path/to/package/dsh-plugin-multi-root-workspace` | pnpm `link:` to a local checkout; its dependencies are already installed in this repository — best for development |
67
- | GitHub / git | `dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace` | Pulls source and builds it on the spot through `prepare`; needs **two** `allowBuilds` answers (this package plus `koffi`); pin a tag |
63
+ | npm registry (recommended) | `dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace` | Pre-built artifacts, ready to use |
64
+ | GitHub | `dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace` | Builds automatically from source on install; `allowBuilds` must name this package too — pin a version |
65
+ | tarball | `dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-<version>.tgz` | Pre-built offline package, handy for air-gapped or offline delivery |
66
+ | From source | `dsh plugin --profile web add /path/to/package/dsh-plugin-multi-root-workspace` | pnpm `link:` to a local checkout — best for development; no `allowBuilds` needed |
68
67
 
69
- **Every install source needs one `allowBuilds` answer the first time.** The plugin carries a native dependency that must be built, `koffi` (the FFI wrapper behind Registry Authority on Windows; installed but unused elsewhere), and pnpm ≥10 runs no dependency lifecycle script by default. So the first `dsh plugin add` fails with `[ERR_PNPM_IGNORED_BUILDS]` and leaves the pending decision in that profile's `pnpm-workspace.yaml`:
68
+ **The first `dsh plugin add` fails with `[ERR_PNPM_IGNORED_BUILDS]`**: add the following line to that profile's `pnpm-workspace.yaml`, then run `add` again:
70
69
 
71
70
  ```yaml
72
71
  allowBuilds:
73
- koffi: true # the placeholder dsh leaves is "set this to true or false"
72
+ koffi: true
74
73
  ```
75
74
 
76
- Set it to `true` and run `add` again. Read that line as **letting the dependency's install script run on your machine** (outside any sandbox the agent runs in) — `koffi`'s script only precompiles locally. Diagnosis steps: [Troubleshooting: the install stops at build approval](./docs/troubleshooting/install-stops-at-build-approval.md).
75
+ If the retry exits 0 but the plugin never lands in `dsh.profile.bundles`, do a `remove` + `add`. Further diagnosis: [troubleshooting](./docs/troubleshooting/install-stops-at-build-approval.md).
77
76
 
78
77
  ### Install from npm (recommended)
79
78
 
@@ -81,18 +80,18 @@ Set it to `true` and run `add` again. Read that line as **letting the dependency
81
80
  dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace
82
81
  ```
83
82
 
84
- This installs pre-built artifacts (the plugin itself is never compiled), but the first install still needs the `allowBuilds` answer above.
83
+ This installs pre-built artifacts; the first install needs the `allowBuilds` addition described above.
85
84
 
86
85
  ### Install from a tarball
87
86
 
88
87
  ```sh
89
88
  pnpm pack @dsh-electron/dsh-plugin-multi-root-workspace
90
89
  # or download the tgz from the GitHub Release assets, e.g.:
91
- # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.5/dsh-electron-dsh-plugin-multi-root-workspace-0.1.5.tgz
92
- dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.5.tgz
90
+ # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.8/dsh-electron-dsh-plugin-multi-root-workspace-0.1.8.tgz
91
+ dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.8.tgz
93
92
  ```
94
93
 
95
- Also pre-built (the plugin itself is never compiled here), handy for air-gapped or offline delivery — and the first `add` needs the same `allowBuilds` answer.
94
+ Also pre-built, handy for air-gapped or offline delivery; the first install needs the same `allowBuilds` addition.
96
95
 
97
96
  ### Install from GitHub
98
97
 
@@ -100,7 +99,7 @@ Also pre-built (the plugin itself is never compiled here), handy for air-gapped
100
99
  dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace
101
100
  ```
102
101
 
103
- With pnpm ≥10 the first `add` fails: a git install pulls **source code rather than build artifacts**, so the package's self-contained `prepare` script must build it on the spot (a direct transpile of `src/`, no type-checking). Follow `dsh`'s guidance and copy the exact package keys pnpm prints into the profile's `pnpm-workspace.yaml` — this source has **two** pending decisions:
102
+ A GitHub install fetches source code and builds it automatically during `add` — no manual build needed — but `allowBuilds` has to cover two entries, this package itself and `koffi`:
104
103
 
105
104
  ```yaml
106
105
  allowBuilds:
@@ -108,93 +107,155 @@ allowBuilds:
108
107
  koffi: true
109
108
  ```
110
109
 
111
- Then run `add` again. Pinning a tag (e.g. `#v0.1.5`) is recommended so a later push cannot silently change what actually runs:
110
+ Pinning a version (e.g. `#v0.1.8`) is recommended so a later push cannot silently change what actually runs:
112
111
 
113
112
  ```sh
114
- dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.5
113
+ dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.8
115
114
  ```
116
115
 
117
- ### Install from a local clone (development & debugging)
116
+ ### Install from a local clone
118
117
 
119
118
  ```sh
120
119
  git clone https://github.com/cherrchen/dsh-plugin-multi-root-workspace.git
121
120
  cd dsh-plugin-multi-root-workspace
122
- export CI=true # without a TTY, pnpm's dependency self-check aborts; see the development workflow §8
121
+ export CI=true
123
122
  pnpm install
124
- pnpm build # produces lib/ (not tracked by Git); the artifact test and installation both need it
125
- dsh plugin --profile web add "$PWD"
123
+ pnpm build
124
+ dsh plugin --profile web add /path/to/repo
126
125
  ```
127
126
 
128
- > **Tip**: if your DSH comes from a source checkout (rather than `npm install -g @deepseek-ai/deepseek-harness`), the `dsh` binary is not on the global PATH — replace `dsh` with `pnpm dsh` in the commands above, e.g. `pnpm dsh plugin --profile web add ...` and `pnpm dsh --profile web`.
127
+ > [!TIP]
128
+ > If `dsh` is not on your PATH (e.g. you run DSH from a source checkout), use `pnpm dsh` instead.
129
129
 
130
130
  ## Running
131
131
 
132
- Development gates, with the result each step should produce:
132
+ Development gates:
133
133
 
134
134
  ```sh
135
- pnpm lint && pnpm typecheck # expect: 0 warnings, 0 errors; both tsconfigs pass
136
- pnpm test # expect: all pass; dialect cases without a local kernel runner skip explicitly, with a reason
137
- pnpm kernel:probe # expect: reports the kernel runners this host has (seatbelt / bwrap / landlock)
138
- pnpm smoke # expect: compose 40/40, behavior 99/99, journey 55/55
139
- pnpm docs:check # expect: 0 errors, 0 warnings
135
+ pnpm lint && pnpm typecheck
136
+ pnpm test
137
+ pnpm kernel:probe
138
+ pnpm smoke
139
+ pnpm docs:check
140
140
  ```
141
141
 
142
142
  Install into a DSH runtime and verify the composition:
143
143
 
144
144
  ```sh
145
- dsh plugin --profile web add "$PWD"
146
- dsh --profile web --dump-config
145
+ dsh plugin --profile web add /path/to/repo
146
+ dsh --profile web --dump-config | grep -A3 'multi-root'
147
+ ```
148
+
149
+ Expected output:
150
+
151
+ ```text
152
+ # == @deepseek-ai/dsh-base, patched by @dsh-electron/dsh-plugin-multi-root-workspace
153
+ - id: sandbox
154
+ name: '@deepseek-ai/dsh-sandbox-local'
155
+ disabled: true
156
+ --
157
+ # == @deepseek-ai/dsh-base, patched by @dsh-electron/dsh-plugin-multi-root-workspace
158
+ - id: fs-sandbox
159
+ name: '@deepseek-ai/dsh-fs-sandbox'
160
+ disabled: true
161
+ --
162
+ # == @dsh-electron/dsh-plugin-multi-root-workspace
163
+ - id: multi-root-compat
164
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/compat'
165
+ - id: multi-root-fs
166
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/fs'
167
+ - id: multi-root-sandbox
168
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/sandbox'
169
+ - id: multi-root-providers
170
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/providers'
171
+ - id: multi-root-scope
172
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/scope'
173
+ config:
174
+ toolAccess: {}
175
+ - id: multi-root-registry
176
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/registry'
177
+ config:
178
+ leasePath: !!js dshHomePath('storages/multi_root_workspace.lock')
179
+ maxAdditionalRoots: 16
180
+ - id: multi-root-instructions
181
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/instructions'
182
+ config:
183
+ maxBytes: 65536
184
+ - id: multi-root-lsp
185
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/lsp'
186
+ - id: multi-root-workspace-files
187
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/workspace-files'
188
+ - id: multi-root-command
189
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace/command'
190
+ - id: multi-root-client
191
+ name: '@dsh-electron/dsh-plugin-multi-root-workspace'
147
192
  ```
148
193
 
149
- Expected: **only** the `fs-sandbox` and `sandbox` rows are replaced by the plugin's `multi-root-fs` / `multi-root-sandbox`, with ten rows inserted (`multi-root-compat` / `fs` / `sandbox` / `scope` / `registry` / `instructions` / `lsp` / `workspace-files` / `command`, plus the client carrier row `multi-root-client`); `bash-sandbox` stays upstream (bash and the PTY backend take their roots from `ctx.sandbox`). After starting `dsh --profile web`, the Folders action appears at the sidebar foot.
194
+ The upstream `fs-sandbox` and `sandbox` rows are disabled and taken over by the plugin's same-named services, and all the plugin's `multi-root-*` rows are mounted. After startup, a **Folders** action appears at the sidebar foot.
150
195
 
151
196
  ## Usage
152
197
 
153
- **The command** (the text entry point; also available headless):
198
+ ### Command
154
199
 
155
200
  ```text
156
- /workspace-folders # list the primary root and the additional roots
157
- /workspace-folders add <absolute path> # with no path, opens the OS directory chooser
158
- /workspace-folders alias 1 payments # name the first additional root
159
- /workspace-folders remove 1 # by ordinal or by path
160
- /workspace-folders reveal 1 # show it in the file manager
201
+ /workspace-folders [list] # list the primary root and the additional roots (access and state included)
202
+ /workspace-folders add <absolute path> [--inherit|--read-only|--read-write] # no path opens the OS directory picker; new registrations default to inherit
203
+ /workspace-folders access <n|path> inherit|read-only|read-write # switch access (widening requires the root to be available right now)
204
+ /workspace-folders alias <n|path> [name] # omitting the name clears the alias
205
+ /workspace-folders remove <n|path> # n is the number list prints
206
+ /workspace-folders reveal <n|path> # show it in the file manager
207
+ /workspace-folders isolate-profiles [--apply] # inspect profile import method and multi-link files under $DSH_HOME; --apply writes copy config
208
+ /workspace-folders help # show the command help
161
209
  ```
162
210
 
163
- After an `add`, `list` prints:
211
+ Paths may be quoted; a path that duplicates an existing root, nests inside one, or is the workspace root itself is rejected.
212
+
213
+ Access is three-state ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md); the [usage guide](./docs/guide/README.en.md#access-levels-the-root-limit-and-per-tool-caps) has the exact semantics): `inherit` follows the session mode — writable under `workspace-write`, refused under `read-only`, unrestricted under `danger-full-access`; `read-only` refuses writes in every mode, including Full-access; a fixed `read-write` root stays writable even in a read-only session. Writable records registered by older plugin versions read back as `inherit`. A primary root accepts at most 16 additional roots (`maxAdditionalRoots` can raise it to 64). A host may also cap a tool for all additional roots via `toolAccess` on the `multi-root-scope` row — e.g. `{ bash: 'read-only' }` keeps bash from ever writing an additional root (it can still write the primary root).
214
+
215
+ After an `add`, `list` prints (a `workspace-write` session, with `website` registered read-only):
164
216
 
165
217
  ```text
166
218
  Workspace root (primary; access follows the current sandbox mode): /home/me/monorepo
167
- 1 /home/me/payments-service [payments]
168
- 2 /home/me/website
169
- Writable additional roots: 2 of 2.
219
+ 1 /home/me/payments-service [payments] (inherit)
220
+ 2 /home/me/website (read-only)
221
+ 2 of 16 additional roots.
222
+ Writable additional roots under the current mode: 1 of 2.
170
223
  ```
171
224
 
172
- **The panel** (Web GUI): the **Folders** dialog at the sidebar foot lists the primary root and the additional roots, and offers add (through the composed directory picker or a typed path), remove, alias, copy path, reveal in the file manager, and move up/down. Its copy is bilingual and follows the interface language. A root's state is shown honestly: `missing` (absent right now) and `redirected` (replaced by a symlink pointing elsewhere) keep their registration but are **not granted**; once the directory is back, a `list` or a panel refresh grants it again — no restart.
225
+ ### Panel
226
+
227
+ The **Folders** dialog at the sidebar foot lists the primary root and the additional roots, and offers add, remove, alias, read-only/read-write switching, copy path, reveal in the file manager, and move up/down.
173
228
 
174
- **The model side**: the additional-root topology reaches the model through a `systemPrompt.context` snapshot attached to every request (same workspace, cwd unchanged) — no introspection tool needed. The full authorization semantics (canonicalization, `recordedPath` against symlink transplants, conflict rejection) live in the [architecture document](./docs/architecture/multi-root-workspace.md).
229
+ ### Model side
230
+
231
+ The additional-root topology is injected into the system prompt so the model knows which directories are available, and the information updates when grants change. No new tool needs to be exposed to the model.
175
232
 
176
233
  ## Project Structure
177
234
 
178
235
  ```text
179
- src/
180
- roots.ts pure rules: canonicalization, conflict validation, record classification (available/missing/redirected/invalid)
181
- registry.ts the root registry: dsh-storage-domain persistence, mutations of one primary root serialized in one queue
182
- registry-lease.ts / registry-lease-win32.ts
183
- cross-process Registry Authority: POSIX flock and a Windows named semaphore (the kernel releases it on process death)
184
- scope.ts ctx.multiRootScope: the single authorization source, plus the model-visible topology snapshot
185
- fs.ts the multi-root filesystem provider (in-process fence, extends the upstream LocalFileSystem)
186
- sandbox.ts the multi-root kernel-sandbox provider (extends the upstream LocalSandboxProvider)
187
- dialects.ts Seatbelt / bwrap / Landlock profile recognition and additional-grant assembly (unrecognized shapes fail loudly)
188
- containment.ts path containment (lexical fast path plus a dev/ino alias fallback)
189
- instructions.ts discovery, incremental delivery, budget and revocation for an additional root's AGENTS.md / CLAUDE.md (top level plus the subdirectories this session touched, injected from agent/pre-step)
190
- compat.ts the multi-root-compat startup gate (version allowlist and mixed-install detection)
191
- compat/ the version-difference adapters: dsh-version / sandbox-confine / agent-instructions / llm-message (optional peers loaded on demand)
192
- command.ts the /workspace-folders command and the host half of the panel RPC
193
- contract.ts the panel wire protocol (zod-validated on both ends, inlinable into the browser bundle)
194
- client/ the browser half: sidebar action, dialog, bilingual dictionaries
195
- tests/ differential parity, real-execution dialect matrix, contract round-trips, component and locale gates, cross-process lease e2e, instruction injection
196
- scripts/ smoke batteries (compose / behavior / journey) plus the docs, kernel-runner, compatibility-contract and upgrade checks
197
- docs/ requirements, architecture, decision records (ADRs), plans, development workflow
236
+ .
237
+ ├── src/
238
+ │ ├── roots.ts pure rules: canonicalization, conflict validation, record classification (available/missing/redirected/invalid)
239
+ │ ├── registry.ts the root registry: dsh-storage-domain persistence, mutations of one primary root serialized in one queue
240
+ │ ├── registry-lease.ts cross-process Registry Authority: POSIX flock (the kernel releases it on process death)
241
+ │ ├── registry-lease-win32.ts ditto, the Windows named-semaphore implementation
242
+ │ ├── scope.ts ctx.multiRootScope: the single authorization source, plus the model-visible topology snapshot
243
+ │ ├── fs.ts the multi-root filesystem provider (in-process fence, extends the upstream LocalFileSystem)
244
+ │ ├── sandbox.ts the multi-root kernel-sandbox provider (extends the upstream LocalSandboxProvider)
245
+ │ ├── dialects.ts Seatbelt / bwrap / Landlock profile recognition and additional-grant assembly (unrecognized shapes fail loudly)
246
+ │ ├── windows-*.ts the Windows root-set ACL runner and restricted-token execution
247
+ │ ├── containment.ts path containment (lexical fast path plus a dev/ino alias fallback)
248
+ │ ├── instructions.ts discovery, incremental delivery, budget and revocation for an additional root's AGENTS.md / CLAUDE.md (top level plus the subdirectories this session touched, injected from agent/pre-step)
249
+ │ ├── lsp.ts a separate LSP workspace initialized per additional root
250
+ │ ├── workspace-files.ts host-side routing and confinement for additional-root file browsing in the Files tab
251
+ │ ├── compat.ts the multi-root-compat startup gate (version allowlist and mixed-install detection)
252
+ │ ├── compat/ the version-difference adapters: dsh-version / sandbox-confine / agent-instructions / llm-message, etc. (optional peers loaded on demand)
253
+ │ ├── command.ts the /workspace-folders command and the host half of the panel RPC
254
+ │ ├── contract.ts the panel wire protocol (zod-validated on both ends, inlinable into the browser bundle)
255
+ │ └── client/ the browser half: sidebar action, the Folders dialog, the Files-tab root switcher, bilingual dictionaries
256
+ ├── tests/ differential parity, real-execution dialect matrix, contract round-trips, component and locale gates, cross-process lease e2e, instruction injection
257
+ ├── scripts/ smoke batteries (compose / behavior / journey) plus the docs, kernel-runner, compatibility-contract and upgrade checks
258
+ └── docs/ requirements, architecture, decision records (ADRs), plans, development workflow
198
259
  ```
199
260
 
200
261
  ## Contributing
@@ -213,8 +274,11 @@ Issues and PRs are welcome:
213
274
  - Single writer per store: only one DSH process at a time may hold the root registry for a given `$DSH_HOME`; another process reports the registry as unavailable (`registry-contended`) and takes over on refresh once the holder exits or crashes. That is deliberate fail-closed behavior, not a race waiting to be fixed ([ADR-0007](./docs/decisions/ADR-0007-registry-authority-lease.md)).
214
275
  - The supported matrix is an exact-version allowlist: on a host whose release is not on the list, or whose core packages disagree about the release, the `fs` / `sandbox` / `registry` / `instructions` / `lsp` / `workspace-files` rows **do not start** and the composition degrades to "the plugin is not installed" ([ADR-0009](./docs/decisions/ADR-0009-dsh-compat-contract.md)).
215
276
  - The Windows multi-root runner inherits upstream partial enforcement, caller-owned directory requirements, and private-temp isolation. Running processes retain their initial roots; later processes use the updated set ([ADR-0011](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
216
- - An additional root has the same rights as the primary root (no per-root read-only), and cannot become the default working directory of bash/PTY (session cwd semantics are unchanged).
217
- - Browse additional roots in the Workspace Folders dialog with manual refresh, expandable directories, and previews of up to 200 text lines. The native primary-root Files tab remains and offers a dialog action where supported. LSP uses the containing additional root as its own workspace; the host must configure the relevant language server ([ADR-0011](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
277
+ - **Network and device paths cannot be additional roots on Windows**: UNC (`\\server\share`), `\\?\`, `\\.\`, and mapped network drives are rejected at registration with `unsupported-path` — the restricted token's capability SID cannot be shown to bind SMB access ([ADR-0011 amendment](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
278
+ - **The confined Windows shell is pwsh; Git Bash is not available**: MSYS cannot start under the write-restricted Low-integrity token (identically under the upstream and plugin runners — an upstream limitation). An upstream-confined child also cannot receive Ctrl+C (it inherits the runner's ignore flag); under the plugin runner Ctrl+C reaches the child and the runner mirrors its exit code.
279
+ - Additional-root access is three-state: new registrations default to `inherit` (they follow the session mode), `--read-only` refuses writes in every mode (including Full-access), and a fixed `--read-write` stays writable even in a read-only session — it is a question of write authority, never read confinement ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)). A running process or persistent terminal keeps the deny set it was started with — a terminal started before a root turned read-only stays unrestricted until restarted; cap and access changes constrain only later calls and new processes ([ADR-0013](./docs/decisions/ADR-0013-per-root-and-per-tool-access.md)); hosts with workingDirectory may use an additional root as the execution directory for later tools, while the session origin and registry identity stay unchanged ([ADR-0012](./docs/decisions/ADR-0012-session-origin-and-effective-cwd.md)).
280
+ - **On Linux hosts where upstream selected Landlock, a restricted Full-access needs bubblewrap**: Landlock's allowlist cannot express "everything writable except these roots", so that combination selects bwrap to spell the deny boundary; on a host without a usable bwrap (not installed, or unprivileged user namespaces disabled), a Full-access command that carries any deny boundary (a read-only root, or the protected `$DSH_HOME`) fails closed at launch. Enable bwrap there, or avoid explicit read-only roots under Full-access on such hosts ([ADR-0017](./docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md)).
281
+ - Browse additional roots from the **Files tab's root switcher**: the header entry lists the primary root and every additional root, with the selected root's full path still shown beside it; selecting one lets a byte-for-byte fork of the upstream Files tree take over the tree area (lazy-loaded directories, native rows and icons, watch-driven auto-refresh, file clicks opening in the host viewer, plus a manual refresh in the header's reload spot), and the header's open-in-app button is simultaneously replaced by a byte-for-byte fork of the upstream control that opens the selected root instead of the primary one; switching back to the primary root restores the native view. The dialog is management-only again. The takeover relies on upstream `data-*` DOM hooks and degrades silently to the native look when they stop matching ([ADR-0015](./docs/decisions/ADR-0015-files-tab-plugin-owned-browsing.md), [ADR-0016](./docs/decisions/ADR-0016-files-tab-native-tree-fork.md)). LSP uses the containing additional root as its own workspace; the host must configure the relevant language server ([ADR-0011](./docs/decisions/ADR-0011-multi-root-workspace-consumers.md)).
218
282
  - The command's own output text is English (a host-side handler has no active locale to consult); the panel is bilingual.
219
283
  - The Workspace Folders panel is a sidebar footer action plus a dialog rather than a full panel: `sidebar.footer.action` is offered by every supported release, whereas `sidebar.panellist` / `main` are not.
220
284
 
@@ -223,6 +287,7 @@ Issues and PRs are welcome:
223
287
  Long-term project documentation lives in [`docs/`](./docs/README.md):
224
288
 
225
289
  - [Changelog](./CHANGELOG.en.md)
290
+ - [User guide](./docs/guide/README.en.md)
226
291
  - [Requirements](./docs/requirements/multi-root-workspace.md)
227
292
  - [Target architecture](./docs/architecture/multi-root-workspace.md)
228
293
  - [Upstream research](./docs/reference/multi-root-workspace-research.md)
@@ -236,5 +301,3 @@ Repository-wide rules for Coding Agents are defined in [`AGENTS.md`](./AGENTS.md
236
301
  ## License
237
302
 
238
303
  [MIT](./LICENSE)
239
-
240
- Dynamic working directories affect relative path resolution and new processes. They do not grant access or change the session origin that owns the root registry. See [ADR-0012](./docs/decisions/ADR-0012-session-origin-and-effective-cwd.md).