@lone-wolf-akela/dsh-sandboxie-redirect 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ Notable changes per release. The format is loose; the version numbers are what
4
+ the DSH compatibility gate reads.
5
+
6
+ ## 0.3.0 — 2026-10-07
7
+
8
+ The distribution changed shape, so this is a breaking release for the earlier
9
+ hand-mounted installations.
10
+
11
+ **Distribution**
12
+
13
+ - The package is now a DSH **bundle**: `dsh.bundle.patch` points at a root
14
+ `cordis.patch.yml` whose rows name the package instead of an absolute Windows
15
+ path. Install with `dsh plugin --profile <name> add @lone-wolf-akela/dsh-sandboxie-redirect`
16
+ (or the in-app Plugins page for the profile a running desktop app owns); the
17
+ profile no longer needs hand-editing.
18
+ - `exports["."]` now resolves to `lib/provider.mjs`. It used to be `lib/host.mjs`,
19
+ which would have mounted the wrong half of the plugin under a package row.
20
+ - Added `LICENSE`, `files`, `publishConfig.access`, `repository`/`author`/`bugs`,
21
+ `os: ["win32"]`, keywords and the peer ranges the compatibility gate reads
22
+ (`@deepseek-ai/dsh-* ^0.2.0-rc.2`, `@deepseek-ai/cordis ^4.0.4`).
23
+ - Removed the `cordis.patch.yml.new` template: with the bundle channel it was the
24
+ one file that could not survive publication (it carried an absolute path).
25
+
26
+ **Configuration layer**
27
+
28
+ - The `permission` row's preset list is computed at load time (`!!js`) from the
29
+ layers before it, instead of restating the three shipped presets. A preset
30
+ another bundle contributes is no longer silently dropped, and an unreadable
31
+ earlier layer falls back to the literal stock three.
32
+ - The preset's display name and description are bilingual:
33
+ `❐ Copy-on-write 写时复制`.
34
+
35
+ **Behaviour**
36
+
37
+ - Diagnostics moved out of the package directory into
38
+ `$DSH_HOME/state/dsh-sandboxie-redirect` (`DSH_SBIE_LOG_DIR` overrides). An
39
+ installed bundle lives in the profile's `node_modules`, possibly served from
40
+ pnpm's content-addressed store, where appending logs is both rude and fragile.
41
+ - The launcher's Node runtime is now located through `process.resourcesPath`
42
+ (the desktop app's own Node) before the `$DSH_HOME/dsh-runtimes` pool, and
43
+ `process.execPath` is only accepted on a non-Electron host. Finding no genuine
44
+ Node now fails loudly with `SANDBOX_UNAVAILABLE` instead of silently spawning
45
+ the Electron binary inside the box.
46
+ - The copy root follows Sandboxie's configured `FileRootPath` (box setting, then
47
+ `[GlobalSettings]`), with `%USER%`/`%SANDBOX%`/`%SystemDrive%`/`%USERPROFILE%`/
48
+ `%WINDIR%` expanded; `DSH_SBIE_ROOT` still overrides it. A template without
49
+ `%SANDBOX%`, or one that cannot be fully expanded, is refused — the plugin then
50
+ keeps the default layout rather than pointing its cleanup at a shared folder.
51
+ - `--manage list` derives the sandbox root from the boxes it lists, and
52
+ `--manage show` resolves a box's own directory the same way.
53
+ - The model-facing note names the same resolved root.
54
+
55
+ **Tooling and docs**
56
+
57
+ - `tools/install.mjs` (the manual channel) derives its copy list from
58
+ `package.json`'s `files`.
59
+ - `test/validate-patch.mjs` accepts both row shapes (package name for bundles,
60
+ absolute path for the manual channel), reads js-yaml from the devDependency /
61
+ `asar-yaml/` / an installed profile, and now EXECUTES the computed preset
62
+ expression against a stub context.
63
+ - The module-level import pin for `lib/tool.mjs` follows relative siblings
64
+ recursively instead of banning them.
65
+ - GitHub Actions runs the Sandboxie-free tests; `npm test`, `npm run
66
+ test:acceptance` and `npm run pack:check` are wired up. The READMEs are
67
+ rewritten around the bundle install.
68
+
69
+ ## 0.2.1
70
+
71
+ The last hand-mounted release: copy the tree into
72
+ `~/.dsh/plugins/dsh-sandboxie-redirect` and add three rows to the profile's
73
+ `cordis.patch.yml`. Functionally it already had the copy-on-write preset, the
74
+ title-bar box name, the permission-dropdown icon and the `sandbox_clear` tool.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-sandboxie-redirect contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # dsh-sandboxie-redirect
2
+
3
+ > **中文版**:[README.zh.md](README.zh.md)
4
+
5
+ A [DSH](https://github.com/deepseek-ai/deepseek-harness) bundle that adds a fourth permission preset, **Copy-on-write (写时复制)**: shell commands run inside a Sandboxie box dedicated to the workspace, so writes inside the workspace land on the real disk as usual, while writes, edits and deletions anywhere else keep succeeding — but only inside that box's copy. The three shipped presets (read-only, workspace-write, danger-full-access) behave exactly as before.
6
+
7
+ ```
8
+ ┌─ your DSH session ────────────────┐
9
+ │ file policy: copy-on-write │
10
+ │ │
11
+ │ shell command ──▶ Sandboxie box ─┼──▶ workspace writes → real disk
12
+ │ (inside the box) (per workspace)┼──▶ writes elsewhere → box copy
13
+ │ │ deletes elsewhere → tombstone in copy
14
+ │ read/write/edit ──▶ host process ┼──▶ unchanged: writable inside the
15
+ │ (outside the box) │ workspace, refused outside it
16
+ └───────────────────────────────────┘
17
+ ```
18
+
19
+ - Windows only, and it needs [Sandboxie-Plus](https://sandboxie-plus.com/) installed (the sandbox service must be running).
20
+ - It is a permission **preset**, not a patch to the DSH application: nothing inside the app is modified.
21
+ - Its purpose is accident prevention, undo, and keeping system directories clean. It is **not** a defence against malicious programs — Sandboxie itself makes no such promise, and a plugin runs with the same access as the app hosting it.
22
+
23
+ ## Terminology
24
+
25
+ | Term | Meaning |
26
+ |---|---|
27
+ | Real disk | Where files actually live (for example `C:\Users\…`). |
28
+ | Sandbox / box | The isolated environment Sandboxie creates. This plugin creates one per workspace. |
29
+ | Copy | The box's private storage for changes to files outside the workspace (under the sandbox root), kept apart from the real files. |
30
+ | Inside / outside the box | Under Copy-on-write, shell commands run inside the box; the DSH app and its file tools (read/write/edit) run outside it. |
31
+ | Merged view | What a command inside the box sees: the copy's content overlaid on the real file. |
32
+ | SandMan | Sandboxie-Plus's GUI, for inspecting, recovering and deleting sandboxes. |
33
+
34
+ ## Installation
35
+
36
+ **Prerequisite:** [Sandboxie-Plus](https://sandboxie-plus.com/) ([source](https://github.com/sandboxie-plus/Sandboxie)). The installer must have created its service; every box this plugin uses is created and managed by Sandboxie.
37
+
38
+ The package is a DSH *bundle*: installing it also adds its configuration layer, so no profile file needs hand-editing.
39
+
40
+ ```powershell
41
+ # install into a profile (the CLI creates it if needed and appends this bundle)
42
+ dsh plugin --profile <profile> add @lone-wolf-akela/dsh-sandboxie-redirect
43
+
44
+ # check the layer without booting, then start
45
+ dsh --profile <profile> --dump-config # expect a "# == @lone-wolf-akela/dsh-sandboxie-redirect" layer
46
+ dsh --profile <profile>
47
+ ```
48
+
49
+ Other supported sources, for a local checkout or an offline machine:
50
+
51
+ ```powershell
52
+ dsh plugin --profile <profile> add C:\path\to\dsh-sandboxie-redirect # a checkout (linked)
53
+ dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.0.tgz
54
+ ```
55
+
56
+ > **Which command for which profile?** The CLI refuses to touch a profile that a running desktop app owns (`profile "desktop" is managed exclusively by the Electron application`). For that profile, install from inside the app: **Settings → Plugins → add a bundle**, and give the package name above. For any other profile, the CLI works normally.
57
+
58
+ Then restart the client, open the permission dropdown, and select **❐ Copy-on-write 写时复制**. The preset appears once per session as needed; the title bar shows the box name after the first shell command.
59
+
60
+ ### Migrating from a hand-mounted copy
61
+
62
+ Earlier revisions were installed by copying the tree into `~/.dsh/plugins/dsh-sandboxie-redirect` and adding rows to `~/.dsh/profiles/<profile>/cordis.patch.yml`. If your profile still carries those rows, **remove them** before switching to the bundle channel: two active rows into the same `dsh.client` package fail the boot ("resolves from multiple active Loader sources"). The rows to delete are the ones whose `name` is an absolute path ending in `lib\provider.mjs`, plus the `sandbox` and `permission` rows this plugin added. `tools/install.mjs` remains in the repository for that manual channel, and it must be run outside the sandbox.
63
+
64
+ ## What it does
65
+
66
+ ### The Copy-on-write preset
67
+
68
+ | Location | Behavior |
69
+ |---|---|
70
+ | Writes, edits, deletions **inside** the workspace | Applied directly to the real disk |
71
+ | Writes, edits **outside** the workspace | The command reports success, but the change lands in that workspace's box copy; the real disk is untouched |
72
+ | Deletions **outside** the workspace | The real file survives; the copy records only a deletion marker |
73
+
74
+ DSH's own file tools (read/write/edit) run outside the box and keep the workspace-write rule: writable inside the workspace, refused outside it. To read a file *as the sandbox sees it*, read it with a shell command — see [Read-side divergence](#read-side-divergence).
75
+
76
+ Under this preset the session's policy text gains a paragraph that tells the model how the mode behaves and what to watch for, so an agent does not have to infer it.
77
+
78
+ ### Sandbox name in the title bar
79
+
80
+ Under Copy-on-write the title bar shows the box name (for example `dsh_able_willow`; hover for the copy's root). It appears only when both hold: this session's preset is Copy-on-write, and this workspace really owns a box — so it shows up after the first command, and disappears when the preset changes or the box is cleaned.
81
+
82
+ ### Permission dropdown icon
83
+
84
+ The preset carries an icon (two overlapping sheets) in the dropdown and the bottom button, following the active theme. The preset's display name starts with `❐` for the same reason: the shipped dropdown takes icons from a closed id→icon map, so the name is the one surface that always renders.
85
+
86
+ ### The `sandbox_clear` tool
87
+
88
+ In-session, `sandbox_clear` inspects or discards this workspace's copy: `mode: "inspect"` reports (file count, size), `mode: "clear"` deletes the copy and its configuration. It works in every permission mode, and can only ever touch boxes Sandboxie attributes to *this* workspace.
89
+
90
+ ## Notes for users
91
+
92
+ - **The `sandbox_permissions` parameter description is not written for this preset.** DSH generates that one text for every preset, and it reads as "the retry after a denial" — under Copy-on-write an out-of-workspace write is never denied (it succeeds into the copy), so no denial marker appears and there is nothing to retry. The session's policy text says so explicitly; escalation is a deliberate "run this one call on the real disk" lever, not a follow-up to a failure.
93
+ - **Read-side divergence (important)**: one path can look different depending on who reads it.
94
+ - Shell commands run inside the box and may see the merged view — if an earlier command wrote that file outside the workspace, the command sees the copy's version.
95
+ - DSH's file tools run outside the box and always see the real disk.
96
+ - So: to see "what a command wrote outside the workspace", read with a shell command; to see "was the real disk changed", read with DSH's file tools (or File Explorer).
97
+ - **Copy location**: ordinary paths at `<sandbox root>\drive\<drive>\<path>`; paths under the user profile at `<sandbox root>\user\current\<relative path>`. Sandboxie's own sandbox folder is followed automatically (its `FileRootPath`, box setting or `[GlobalSettings]`, with `%USER%`/`%SANDBOX%` expanded); `DSH_SBIE_ROOT` overrides that if you need it (see [Environment](#environment)).
98
+ - **Disk usage**: copies are not reclaimed automatically and keep growing.
99
+ - **`C:\Windows\Temp`**: modifying an *existing* file there may be refused — native Sandboxie behavior, not this plugin's. Creating and deleting files there works.
100
+ - **Concurrency**: concurrent commands in one workspace share one box and see each other's changes.
101
+ - **Escalation**: if a command escalates for wider permissions, that single call runs entirely outside the box; the next command returns to the box. Escalation is not a way to make one file real.
102
+ - **Write-back**: copy changes are not merged back automatically. Recover files in SandMan; `sandbox_clear` only deletes.
103
+ - **Uninstalling** the bundle removes the preset and the rows, but the boxes and their copies stay on disk. Clean them first (`sandbox_clear`, or `--manage clean`) if you want the space back.
104
+
105
+ ## Managing boxes manually
106
+
107
+ ```powershell
108
+ node bin\dsh-sbie-run.mjs --manage list # list managed boxes
109
+ node bin\dsh-sbie-run.mjs --manage clean "<workspace>" # clean one workspace's boxes
110
+ node bin\dsh-sbie-run.mjs --manage delete-box <box> # delete one box
111
+ ```
112
+
113
+ `--manage` must run outside the box (an ordinary terminal, or a `danger-full-access` session): a process inside a box cannot see Sandboxie's box list. The same operations are available in SandMan.
114
+
115
+ ## Environment
116
+
117
+ All optional; each exists as an escape hatch rather than a configuration surface.
118
+
119
+ | Variable | Effect |
120
+ |---|---|
121
+ | `DSH_SBIE_NODE` | The `node.exe` that runs the launcher. Set it if the plugin cannot find one (the desktop app's own runtime, then `$DSH_HOME/dsh-runtimes`, are searched automatically). |
122
+ | `DSH_SBIE_ROOT` | Overrides the sandbox root. Normally unnecessary: Sandboxie's configured `FileRootPath` is followed. Default `%SystemDrive%\Sandbox\%USERNAME%`. |
123
+ | `DSH_SBIE_LOG_DIR` | Where diagnostics land. Default `$DSH_HOME/state/dsh-sandboxie-redirect`. |
124
+ | `DSH_SBIE_INI` | The Sandboxie configuration file to read. Default `%SystemDrive%\Windows\Sandboxie.ini`. |
125
+ | `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | A `koffi.node` (or a directory holding one) for the launcher, when the app's copy cannot be found. |
126
+ | `DSH_SBIE_BOX` / `DSH_SANDBOX_ROOT` | **Set by the plugin** inside a command; authoritative for "which box am I in" — never derive the name from the workspace path. |
127
+
128
+ Diagnostics: `provider.log`, `host.log`, `tool.log` under the log directory above. The host log is capped at 512 KB and records only state transitions.
129
+
130
+ ## How it works (short)
131
+
132
+ - The bundle layer disables the stock `sandbox` row (`@deepseek-ai/dsh-sandbox-local`) and inserts this package's provider, which subclasses it: only the Copy-on-write *preset* is diverted, every other mode delegates to the stock ACL path. A configured `runnerCommand` cannot do this — it would apply to every mode.
133
+ - The same layer merges `copy-on-write` into the `permission` preset list, keeping whatever the layers before it contributed.
134
+ - Shell commands are then run through `bin/dsh-sbie-run.mjs`, a plain-Node child process that holds the Sandboxie FFI. Sandboxie builds processes through its service and drops the standard handles, so stdio is relayed over a loopback TCP channel with length-prefixed frames.
135
+ - Box names are a deterministic word pair derived from the workspace path (`dsh_<adjective>_<noun>`), so no state file is needed to find a workspace's box; a collision steps to the next candidate, and a name is only adopted when Sandboxie's own configuration says that workspace owns it.
136
+ - Only a failure to load the harness packages is fatal, and that case registers a provider that fails **closed** — refusing to run commands rather than running them unconfined.
137
+
138
+ ## Development
139
+
140
+ See [AGENTS.md](AGENTS.md) for the architecture and the hard-won constraints. The short version:
141
+
142
+ ```powershell
143
+ node test\unit.mjs # pure logic
144
+ node test\host-projection.mjs # the title-bar projection, without the harness bundle
145
+ node test\validate-patch.mjs # this bundle's layer: structure, and the preset merge actually evaluated
146
+ node test\run-tests.mjs # acceptance: real boxes, real redirection (outside any box, medium IL)
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT — see [LICENSE](LICENSE).
package/README.zh.md ADDED
@@ -0,0 +1,150 @@
1
+ # dsh-sandboxie-redirect
2
+
3
+ > **English**: [README.md](README.md)
4
+
5
+ 给 [DSH](https://github.com/deepseek-ai/deepseek-harness) 增加第四种权限预设 **「写时复制」** 的 bundle:shell 命令跑在本工作区专属的 Sandboxie 沙盒里,工作区内的写入照常落到真实磁盘;工作区外的写入、修改、删除不会失败,但只落进该沙盒的副本。原有三个预设(只读 / 仅工作区 / 完全权限)行为不变。
6
+
7
+ ```
8
+ ┌─ 你的 DSH 会话 ───────────────────┐
9
+ │ 文件策略:写时复制 │
10
+ │ │
11
+ │ shell 命令 ──▶ Sandboxie 沙盒 ───┼──▶ 工作区内写入 → 真实磁盘
12
+ │ (在沙盒内) (每工作区一个) ┼──▶ 其它位置写入 → 沙盒副本
13
+ │ │ 其它位置删除 → 副本里只留删除标记
14
+ │ 读/写/编辑 ──▶ 宿主进程(盒外) ┼──▶ 规则不变:工作区内可写,区外拒绝
15
+ └───────────────────────────────────┘
16
+ ```
17
+
18
+ - 仅 Windows,且需要先装 [Sandboxie-Plus](https://sandboxie-plus.com/)(其服务须在运行)。
19
+ - 接入方式是权限**预设**,不修改 DSH 应用本体。
20
+ - 用途是防误写、可撤销、保持系统目录干净。**不用于防御恶意程序**——Sandboxie 不提供该保证,而插件与承载它的应用拥有同等权限。
21
+
22
+ ## 术语
23
+
24
+ | 词 | 含义 |
25
+ |---|---|
26
+ | 真实磁盘 | 系统里实际存放文件的地方(例如 `C:\Users\…`)。 |
27
+ | 沙盒 / 盒 | Sandboxie 创建的隔离环境;本插件为每个工作区创建一个。 |
28
+ | 副本 | 沙盒对"工作区外文件改动"的私有存储(位于沙盒根下),与真实文件分开。 |
29
+ | 盒内 / 盒外 | 写时复制下,shell 命令在盒内运行;DSH 应用本体与它的文件工具(读/写/编辑)在盒外。 |
30
+ | 合并视图 | 盒内命令读文件时,沙盒把副本内容叠加到真实文件上呈现。 |
31
+ | SandMan | Sandboxie-Plus 自带的图形管理界面,可查看、恢复、删除沙盒。 |
32
+
33
+ ## 安装
34
+
35
+ **前置条件**:[Sandboxie-Plus](https://sandboxie-plus.com/)([源码](https://github.com/sandboxie-plus/Sandboxie))。安装后它的服务须在运行;本插件的所有沙盒都由 Sandboxie 创建与管理。
36
+
37
+ 本包是 DSH 的 **bundle**(自带配置层),装进去就自动挂载,**不需要手改 profile 文件**。
38
+
39
+ ```powershell
40
+ # 装进某个 profile(不存在会创建),并自动加入该 profile 的 bundle 列表
41
+ dsh plugin --profile <profile> add @lone-wolf-akela/dsh-sandboxie-redirect
42
+
43
+ # 先不启动,核对配置层,再启动
44
+ dsh --profile <profile> --dump-config # 应看到一行 "# == @lone-wolf-akela/dsh-sandboxie-redirect"
45
+ dsh --profile <profile>
46
+ ```
47
+
48
+ 也支持本地目录 / 离线 tarball:
49
+
50
+ ```powershell
51
+ dsh plugin --profile <profile> add C:\path\to\dsh-sandboxie-redirect # 本地 checkout(link 方式)
52
+ dsh plugin --profile <profile> add .\lone-wolf-akela-dsh-sandboxie-redirect-0.3.0.tgz
53
+ ```
54
+
55
+ > **哪个 profile 用哪条路?** 正在运行的桌面应用独占的 profile(`desktop`)会被 CLI 拒绝操作(`profile "desktop" is managed exclusively by the Electron application`)。这种 profile 请在**应用内**装:**设置 → 插件 → 添加 bundle**,填上面的包名。其它 profile 用 CLI 即可。
56
+
57
+ 装完重启客户端,在权限下拉里选择 **❐ Copy-on-write 写时复制**。沙盒名称会在第一条 shell 命令之后出现在标题栏。
58
+
59
+ ### 从"手工挂载"迁移
60
+
61
+ 早期版本是"把目录拷到 `~/.dsh/plugins/dsh-sandboxie-redirect` + 在 `~/.dsh/profiles/<profile>/cordis.patch.yml` 里加行"。切到 bundle 通道前**必须先删掉那些行**:同一个 `dsh.client` 包有两个活动 loader 行会让启动直接失败("resolves from multiple active Loader sources")。要删的是:`name` 为绝对路径、以 `lib\provider.mjs` 结尾的那一行,以及本插件加的 `sandbox`、`permission` 两行。仓库里的 `tools/install.mjs` 仍然保留给手工通道使用,且必须在沙盒外运行。
62
+
63
+ ## 功能
64
+
65
+ ### 写时复制预设
66
+
67
+ | 位置 | 行为 |
68
+ |---|---|
69
+ | 工作区**内**的写入、修改、删除 | 直接作用于真实磁盘 |
70
+ | 工作区**外**的写入、修改 | 命令报成功,但改动只落在该工作区的副本里;真实磁盘不受影响 |
71
+ | 工作区**外**的删除 | 真实文件保留;副本中只记录删除标记 |
72
+
73
+ DSH 的文件工具(读/写/编辑)在盒外运行,仍按「仅工作区」判定:工作区内可写、区外拒绝。想读"沙盒视角"的文件,用 shell 命令读(见下文「读侧分歧」)。
74
+
75
+ 该预设下,会话的策略文本会追加一段说明,告知模型本模式的行为与注意事项,不必让模型自己推断。
76
+
77
+ ### 标题栏的沙箱名称
78
+
79
+ 写时复制模式下,标题栏显示当前沙盒名称(如 `dsh_able_willow`,悬停可见副本根路径)。它只在两个条件同时满足时出现:该会话的预设是写时复制,且该工作区确已创建沙盒。因此第一条命令后才出现,切换预设或清理沙盒后消失。
80
+
81
+ ### 权限下拉图标
82
+
83
+ 「写时复制」在下拉与底部按钮中带有图标(复制双矩形),随主题配色。预设名以 `❐` 开头也是同一个原因:出厂下拉的图标来自一张封闭的 id→图标表,自定义预设没有图标,而名字是唯一一定渲染的表面。
84
+
85
+ ### sandbox_clear 工具
86
+
87
+ 会话内可用 `sandbox_clear` 查看或清除本工作区的副本:`mode: "inspect"` 仅查看(文件数、占用),`mode: "clear"` 删除副本与配置。任何权限模式下都可用,且只能操作 Sandboxie 认定属于**本工作区**的沙盒。
88
+
89
+ ## 用户注意事项
90
+
91
+ - **`sandbox_permissions` 的参数描述不是为本预设写的。** 那段文字由 DSH 统一生成、所有预设共用,读起来像"被拒之后的补救"——而写时复制下区外写入**不会被拒**(它会成功落进副本),所以既不会出现拒绝标记,也没有"被拒的命令"可重试。会话的策略文本会明确说明这一点:升权是"这一次调用直接作用于真实磁盘"的主动手段,不是失败的后续动作。
92
+ - **读侧分歧(重要)**:同一个路径,不同工具看到的内容可能不同。
93
+ - shell 命令在盒内运行,读到的是合并视图:如果之前有命令在工作区外写过该文件,命令看到的是副本里的版本。
94
+ - DSH 的文件工具在盒外运行,读到的一直是真实磁盘上的版本。
95
+ - 因此:验证"命令在工作区外写了什么"用 shell 命令读;验证"真实磁盘有没有被改动"用 DSH 的文件工具(或资源管理器)读。
96
+ - **副本位置**:普通路径在 `<沙盒根>\drive\<盘符>\<路径>`;用户目录下的路径在 `<沙盒根>\user\current\<相对路径>`。Sandboxie 自己配置的沙盒目录会被自动跟随(`FileRootPath`,盒级或 `[GlobalSettings]`,含 `%USER%`/`%SANDBOX%` 展开);需要时可用 `DSH_SBIE_ROOT` 覆盖(见[环境变量](#环境变量))。
97
+ - **磁盘占用**:副本不自动回收,会持续累积。
98
+ - **`C:\Windows\Temp`**:修改其中**已存在**的文件可能被拒(Sandboxie 原生行为,非本插件所致);新建与删除不受影响。
99
+ - **并发**:同一工作区的并发命令共用一个沙盒,互相可见。
100
+ - **升权**:写时复制下,若某命令需要更大权限而发起升权,该次调用会整体在盒外运行;下一条命令回到盒内。升权不是"把某个文件变真"的手段。
101
+ - **写回**:副本改动默认不回灌真实磁盘。需要恢复时在 SandMan 中查看并恢复;`sandbox_clear` 只做删除,不做写回。
102
+ - **卸载** bundle 只会移除预设与配置行,**沙盒与副本仍留在磁盘上**。想收回空间就先清理(`sandbox_clear`,或 `--manage clean`)。
103
+
104
+ ## 手动管理沙盒
105
+
106
+ ```powershell
107
+ node bin\dsh-sbie-run.mjs --manage list # 列出受管沙盒
108
+ node bin\dsh-sbie-run.mjs --manage clean "<工作区>" # 清理某工作区的沙盒
109
+ node bin\dsh-sbie-run.mjs --manage delete-box <沙盒名> # 删除指定沙盒
110
+ ```
111
+
112
+ `--manage` 必须在盒外运行(普通终端,或 `danger-full-access` 会话):盒内进程看不到沙盒列表。同样的操作也可以在 SandMan 里做。
113
+
114
+ ## 环境变量
115
+
116
+ 全部可选,都是"逃生阀"而非配置面。
117
+
118
+ | 变量 | 作用 |
119
+ |---|---|
120
+ | `DSH_SBIE_NODE` | 跑启动器的 `node.exe`。插件会自动去找(桌面应用自带的运行时,其次 `$DSH_HOME/dsh-runtimes`),找不到时用它指定。 |
121
+ | `DSH_SBIE_ROOT` | 覆盖沙盒根。通常不需要:Sandboxie 配置的 `FileRootPath` 会被自动跟随。默认 `%SystemDrive%\Sandbox\%USERNAME%`。 |
122
+ | `DSH_SBIE_LOG_DIR` | 诊断日志目录。默认 `$DSH_HOME/state/dsh-sandboxie-redirect`。 |
123
+ | `DSH_SBIE_INI` | 要读取的 Sandboxie 配置文件。默认 `%SystemDrive%\Windows\Sandboxie.ini`。 |
124
+ | `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | 启动器要用的 `koffi.node`(或含它的目录),当找不到应用自带的那份时使用。 |
125
+ | `DSH_SBIE_BOX` / `DSH_SANDBOX_ROOT` | **由插件在命令内注入**;"我在哪个盒里"以它们为准,永远不要用工作区路径去推导盒名。 |
126
+
127
+ 诊断文件:上述日志目录下的 `provider.log`、`host.log`、`tool.log`。`host.log` 上限 512 KB,只记状态迁移。
128
+
129
+ ## 实现要点(简述)
130
+
131
+ - bundle 层禁用原 `sandbox` 行(`@deepseek-ai/dsh-sandbox-local`)并插入本包的 provider,它继承原 provider:只把「写时复制」这一预设分流,其余模式一律委托原 ACL 路径。用 `runnerCommand` 做不到这件事——那会作用于所有模式。
132
+ - 同一层把 `copy-on-write` **合并**进 `permission` 的预设目录,保留之前各层贡献的条目。
133
+ - shell 命令经 `bin/dsh-sbie-run.mjs`——一个持有 Sandboxie FFI 的纯 Node 子进程。Sandboxie 由服务创建进程并丢弃标准句柄,因此 stdio 走回环 TCP + 长度前缀帧回传。
134
+ - 盒名由工作区路径确定性派生成词对(`dsh_<形容词>_<名词>`),不需要状态文件;冲突时按候选序列顺延,且只有当 Sandboxie 配置表明该工作区确实拥有某盒时才采用它。
135
+ - 只有"加载 harness 包失败"是致命错误,此时注册的 provider 会**失败关闭**:拒绝执行命令,而不是不加限制地执行。
136
+
137
+ ## 开发
138
+
139
+ 架构与踩过的坑见 [AGENTS.md](AGENTS.md)。常用命令:
140
+
141
+ ```powershell
142
+ node test\unit.mjs # 纯逻辑
143
+ node test\host-projection.mjs # 标题栏投影(不依赖 harness 包)
144
+ node test\validate-patch.mjs # 本 bundle 的层:结构 + 真的执行预设合并表达式
145
+ node test\run-tests.mjs # 验收:真沙盒、真重定向(须在盒外、Medium IL)
146
+ ```
147
+
148
+ ## 许可
149
+
150
+ MIT —— 见 [LICENSE](LICENSE)。
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-sbie-run — DSH sandbox runner backed by Sandboxie-Plus.
4
+ *
5
+ * Wire it into DSH by pointing the `sandbox` plugin row at this file:
6
+ *
7
+ * - id: sandbox
8
+ * name: '@deepseek-ai/dsh-sandbox-local'
9
+ * config:
10
+ * runnerCommand: ['<node.exe>', '<plugin>/bin/dsh-sbie-run.mjs']
11
+ * runnerFailureSignatures: ['dsh-sbie-run: ']
12
+ *
13
+ * See README.zh.md for the full wiring block and the semantics.
14
+ */
15
+ import { manage } from "../lib/cli.mjs";
16
+ import { main } from "../lib/run.mjs";
17
+
18
+ const argv = process.argv.slice(2);
19
+ if (argv[0] === "--manage") {
20
+ process.exitCode = manage(argv.slice(1));
21
+ } else {
22
+ await main(argv);
23
+ }
@@ -0,0 +1,79 @@
1
+ # dsh-sandboxie-redirect — the bundle layer.
2
+ #
3
+ # `dsh plugin --profile <name> add @lone-wolf-akela/dsh-sandboxie-redirect` appends this package
4
+ # to the profile's `dsh.profile.bundles`; the next profile boot applies THIS
5
+ # layer. Rows reference the package by NAME, not by path: Node resolution finds
6
+ # the installed code through the profile's package graph, so the same file works
7
+ # from npm, a tarball, a `link:` checkout or `github:` install.
8
+ #
9
+ # Two consequences of the patch vocabulary, both load-bearing:
10
+ #
11
+ # * On a NON-insert patch, `name` is a GUARD rather than a reassignment:
12
+ # applyEntryPatches keeps `id` plus the remaining overrides and skips the
13
+ # whole patch when `name` does not equal the target row's name. The stock
14
+ # provider row therefore cannot be re-pointed in place — it is DISABLED
15
+ # here, and ours is inserted as its own row.
16
+ #
17
+ # * A patch replaces a row's entire `config` (no deep merge). Restating the
18
+ # stock presets as literals would therefore silently DISCARD any preset a
19
+ # later DSH version or another bundle contributes. So `presets` is computed:
20
+ # it reads whatever the layers before this one composed and merges ours in,
21
+ # falling back to the literal stock three when that composition cannot be
22
+ # read. The failure mode is the old behaviour — never a preset directory
23
+ # that is missing the shipped presets.
24
+ #
25
+ # The plugin's own README documents what the preset does; the reasoning behind
26
+ # this file's shape lives in AGENTS.md.
27
+
28
+ # The stock shell-sandbox provider is replaced by ours. `runnerCommand` cannot
29
+ # do this job: a configured runner applies to EVERY mode, whereas only the
30
+ # copy-on-write *preset* should be diverted.
31
+ - id: sandbox
32
+ name: "@deepseek-ai/dsh-sandbox-local"
33
+ disabled: true
34
+
35
+ - id: permission
36
+ name: "@deepseek-ai/dsh-permission-presets"
37
+ config:
38
+ # The leading glyph in the display name is deliberate and load-bearing: the
39
+ # shipped dropdown takes its icons from a CLOSED id->icon map, so a custom
40
+ # preset renders with none, and the label span is not a stable handle for a
41
+ # DOM shim (its shape changes when the row is selected). The name is host
42
+ # config rendered verbatim in every surface, so the glyph belongs here.
43
+ # The preset also deliberately bundles the LEGAL `workspace-write` mode: the
44
+ # preset identity is what selects the redirect, so the session projection
45
+ # schema, SANDBOX_MODES, the file fence and the escalation tables all stay
46
+ # untouched.
47
+ presets: !!js >-
48
+ (() => {
49
+ const copyOnWrite = {
50
+ sandbox: "workspace-write",
51
+ approval: "ask",
52
+ name: "❐ Copy-on-write 写时复制",
53
+ description: "Writes inside the workspace go to the real disk; writes, edits and deletes anywhere else still succeed but land in this workspace's Sandboxie copy, which SandMan can inspect, recover or discard. 工作区内的写入照旧落到真实磁盘;工作区外的写入、修改、删除不会失败,改落到本工作区的 Sandboxie 副本里。"
54
+ };
55
+ const stock = {
56
+ "read-only": { sandbox: "read-only", approval: "ask" },
57
+ "workspace-write": { sandbox: "workspace-write", approval: "ask" },
58
+ "danger-full-access": { sandbox: "danger-full-access", approval: "never" }
59
+ };
60
+ let base;
61
+ try {
62
+ base = [...ctx.loader.entries()].find((entry) => entry.options?.id === "permission")?.options?.config?.presets;
63
+ } catch {}
64
+ const usable = base !== undefined && base !== null && ["read-only", "workspace-write", "danger-full-access"].every((key) => base[key] !== undefined);
65
+ const source = usable ? base : stock;
66
+ const keys = Object.keys(source).filter((key) => key !== "copy-on-write");
67
+ const after = keys.indexOf("workspace-write");
68
+ keys.splice(after === -1 ? keys.length : after + 1, 0, "copy-on-write");
69
+ return Object.fromEntries(keys.map((key) => [key, key === "copy-on-write" ? copyOnWrite : source[key]]));
70
+ })()
71
+
72
+ # ONE row into this package, and that is a hard requirement: the client-modules
73
+ # registry composes a `dsh.client` package only while exactly ONE of its loader
74
+ # entries is active, and it fails the whole boot otherwise. The header chip, the
75
+ # policy note and the `sandbox_clear` tool are plain imports wired from this
76
+ # row's constructor (see wireCompanions() in lib/provider.mjs).
77
+ - insert:
78
+ - id: sandbox-copy-on-write
79
+ name: "@lone-wolf-akela/dsh-sandboxie-redirect"