@lone-wolf-akela/dsh-sandboxie-redirect 0.3.1 → 0.3.3
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 +46 -0
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/dsh-locate.mjs +302 -0
- package/lib/koffi.mjs +35 -13
- package/lib/tool.mjs +9 -1
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
Notable changes per release. The format is loose; the version numbers are what
|
|
4
4
|
the DSH compatibility gate reads.
|
|
5
5
|
|
|
6
|
+
## 0.3.3 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
**`sandbox_clear` mode `status` never worked**
|
|
9
|
+
|
|
10
|
+
- `execute` referenced `workspace` in the `status` early return while the
|
|
11
|
+
`const workspace = …` that names it was declared below, in the same function
|
|
12
|
+
body. A `const` in its temporal dead zone throws on *any* earlier reference, so
|
|
13
|
+
`mode: "status"` failed on every call with `ReferenceError: Cannot access
|
|
14
|
+
'workspace' before initialization`. The declaration now precedes every use,
|
|
15
|
+
which is also what `status` was always meant to report: the canonical workspace
|
|
16
|
+
alongside the projection registry's state for the calling session.
|
|
17
|
+
- The defect came in with the 0.2.1 import, so it shipped in 0.3.0, 0.3.1 and
|
|
18
|
+
0.3.2; `inspect` and `clear` were unaffected, because they use `workspace`
|
|
19
|
+
after the declaration.
|
|
20
|
+
- New `test/tool-execute.mjs` drives all three modes of the real tool offline
|
|
21
|
+
(only the harness loader, the Node lookup and `spawnSync` are redirected), so
|
|
22
|
+
this — and any future runtime failure in a mode nobody exercises by hand — is a
|
|
23
|
+
failed test rather than a report from a user. It reproduces the
|
|
24
|
+
`ReferenceError` on the pre-fix source.
|
|
25
|
+
|
|
26
|
+
## 0.3.2 — 2026-10-13
|
|
27
|
+
|
|
28
|
+
**DeepSeek Harness installation lookup**
|
|
29
|
+
|
|
30
|
+
- The launcher no longer assumes DSH is installed in
|
|
31
|
+
`%LOCALAPPDATA%\Programs\DeepSeek Harness`. `koffi.node` ships *inside* the
|
|
32
|
+
installation, so a relocated DSH failed every command with "koffi native addon
|
|
33
|
+
not found". The new `lib/dsh-locate.mjs` finds the installation instead: the
|
|
34
|
+
interpreter's own path walked upwards (the host starts the launcher with the
|
|
35
|
+
Node the app ships, so that path points back at the installation), then the
|
|
36
|
+
installer's uninstall entry — `InstallLocation`, `DisplayIcon` and
|
|
37
|
+
`UninstallString` are all accepted, and it is found by display name because
|
|
38
|
+
the key is a GUID — then the conventional folders and `PATH`.
|
|
39
|
+
- `DSH_SBIE_KOFFI` and `DSH_SBIE_KOFFI_ROOT` remain the explicit overrides, and
|
|
40
|
+
the failure message now says what it looked for and where.
|
|
41
|
+
- New `test/dsh-locate.mjs` (18 offline checks) pins the source order and
|
|
42
|
+
includes a regression case for an installation outside every default folder.
|
|
43
|
+
|
|
44
|
+
**Packaging**
|
|
45
|
+
|
|
46
|
+
- `bin` no longer carries a leading `./`. npm's `@npmcli/package-json`
|
|
47
|
+
normalizes that away and then reports `"bin[dsh-sbie-run]" script name
|
|
48
|
+
bin/dsh-sbie-run.mjs was invalid and removed` on every publish; the emitted
|
|
49
|
+
path was already the normalized one, so this removes a misleading warning and
|
|
50
|
+
nothing else. `test/unit.mjs` pins the form.
|
|
51
|
+
|
|
6
52
|
## 0.3.1 — 2026-10-12
|
|
7
53
|
|
|
8
54
|
**Sandboxie installation lookup**
|
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ A [DSH](https://github.com/deepseek-ai/deepseek-harness) bundle that adds a four
|
|
|
42
42
|
|
|
43
43
|
**Any installation folder works — there is nothing to configure.** The plugin does not assume Sandboxie lives in one fixed directory: it reads Sandboxie's own registry entries first (the `SbieSvc` service's `ImagePath` is the authority — an installation that can create a box always has it, followed by the driver service and the installer's uninstall entry), then the conventional folders and `PATH`, and only as a last resort sweeps well-known parent folders and each fixed drive's root for a folder named after Sandboxie that holds `SbieDll.dll`. Another drive, a per-user folder and a portable directory all work. Only when every one of those comes up empty does it report "not installed", and that message names `DSH_SBIE_DIR` (see [Environment](#environment)) as the way to point at it directly.
|
|
44
44
|
|
|
45
|
+
**DSH's own installation folder is not assumed either.** The `koffi.node` the launcher needs ships *inside* DSH, under its `resources\app.asar.unpacked`, and the launcher is a plain Node process with no `process.resourcesPath`. So the plugin locates DSH itself: the Node running the launcher (the desktop app's own runtime lives inside the installation, so that path points back at it), then the installer's uninstall entry, then the conventional folders and `PATH`. DSH installed on another drive works too.
|
|
46
|
+
|
|
45
47
|
The package is a DSH *bundle*: installing it also adds its configuration layer, so no profile file needs hand-editing.
|
|
46
48
|
|
|
47
49
|
```powershell
|
|
@@ -92,7 +94,7 @@ The preset carries an icon (two overlapping sheets) in the dropdown and the bott
|
|
|
92
94
|
|
|
93
95
|
### The `sandbox_clear` tool
|
|
94
96
|
|
|
95
|
-
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.
|
|
97
|
+
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. A diagnostic `mode: "status"` reports what the session projection registry holds for the calling session — it separates "the fold never ran" from "the client never got it", so it is the first thing to look at when the title-bar box name is missing (it only actually worked from 0.3.3 on).
|
|
96
98
|
|
|
97
99
|
## Notes for users
|
|
98
100
|
|
|
@@ -130,7 +132,7 @@ All optional; each exists as an escape hatch rather than a configuration surface
|
|
|
130
132
|
| `DSH_SBIE_ROOT` | Overrides the sandbox root. Normally unnecessary: Sandboxie's configured `FileRootPath` is followed. Default `%SystemDrive%\Sandbox\%USERNAME%`. |
|
|
131
133
|
| `DSH_SBIE_LOG_DIR` | Where diagnostics land. Default `$DSH_HOME/state/dsh-sandboxie-redirect`. |
|
|
132
134
|
| `DSH_SBIE_INI` | The Sandboxie configuration file to read. Default `%SystemDrive%\Windows\Sandboxie.ini`. |
|
|
133
|
-
| `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | A `koffi.node` (or a directory holding one) for the launcher
|
|
135
|
+
| `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | A `koffi.node` (or a directory holding one) for the launcher. Normally unnecessary: DSH's own installation folder is located automatically (see [Installation](#installation)). |
|
|
134
136
|
| `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. |
|
|
135
137
|
|
|
136
138
|
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.
|
package/README.zh.md
CHANGED
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
|
|
42
42
|
**装在哪儿都行,不需要配置。** 插件不假设 Sandboxie 在某个固定目录:它先读 Sandboxie 自己的注册表项(`SbieSvc` 服务的 `ImagePath` 最权威——能建沙盒的安装必然有它,其次是驱动服务与卸载项),再看 `%ProgramFiles%\Sandboxie-Plus` 等常规位置与 `PATH`,最后才按名字扫一遍常见父目录与各固定磁盘根目录,找含 `SbieDll.dll` 的文件夹。装在别的盘、用户目录、便携目录都可以。只有全部落空时才会报 "not installed",此时用 `DSH_SBIE_DIR` 直接指定(见[环境变量](#环境变量))。
|
|
43
43
|
|
|
44
|
+
**DSH 自己的安装目录同样不假定。** 启动器要用的 `koffi.node` 是随 DSH 一起装在它 `resources\app.asar.unpacked` 里的,而启动器是普通 Node 进程(没有 `process.resourcesPath`)。插件会自己定位 DSH:先看运行启动器的那个 node(桌面应用自带的运行时就在安装目录里,这条路径本身就指回安装位置),再看安装器留下的卸载项,最后才是常规目录与 `PATH`。DSH 装在别的盘也能正常工作。
|
|
45
|
+
|
|
44
46
|
本包是 DSH 的 **bundle**(自带配置层),装进去就自动挂载,**不需要手改 profile 文件**。
|
|
45
47
|
|
|
46
48
|
```powershell
|
|
@@ -91,7 +93,7 @@ DSH 的文件工具(读/写/编辑)在盒外运行,仍按「仅工作区
|
|
|
91
93
|
|
|
92
94
|
### sandbox_clear 工具
|
|
93
95
|
|
|
94
|
-
会话内可用 `sandbox_clear` 查看或清除本工作区的副本:`mode: "inspect"` 仅查看(文件数、占用),`mode: "clear"` 删除副本与配置。任何权限模式下都可用,且只能操作 Sandboxie
|
|
96
|
+
会话内可用 `sandbox_clear` 查看或清除本工作区的副本:`mode: "inspect"` 仅查看(文件数、占用),`mode: "clear"` 删除副本与配置。任何权限模式下都可用,且只能操作 Sandboxie 认定属于**本工作区**的沙盒。另有诊断用的 `mode: "status"`:它报告本会话投影登记的状态,用来分辨"投影没被驱动"和"客户端没收到"——标题栏的沙箱名称不显示时先看它(0.3.3 起才真正可用)。
|
|
95
97
|
|
|
96
98
|
## 用户注意事项
|
|
97
99
|
|
|
@@ -129,7 +131,7 @@ node bin\dsh-sbie-run.mjs --manage delete-box <沙盒名> # 删除指定沙
|
|
|
129
131
|
| `DSH_SBIE_ROOT` | 覆盖沙盒根。通常不需要:Sandboxie 配置的 `FileRootPath` 会被自动跟随。默认 `%SystemDrive%\Sandbox\%USERNAME%`。 |
|
|
130
132
|
| `DSH_SBIE_LOG_DIR` | 诊断日志目录。默认 `$DSH_HOME/state/dsh-sandboxie-redirect`。 |
|
|
131
133
|
| `DSH_SBIE_INI` | 要读取的 Sandboxie 配置文件。默认 `%SystemDrive%\Windows\Sandboxie.ini`。 |
|
|
132
|
-
| `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | 启动器要用的 `koffi.node
|
|
134
|
+
| `DSH_SBIE_KOFFI` / `DSH_SBIE_KOFFI_ROOT` | 启动器要用的 `koffi.node`(或含它的目录)。通常不需要:DSH 的安装位置会被自动定位(见[安装](#安装))。 |
|
|
133
135
|
| `DSH_SBIE_BOX` / `DSH_SANDBOX_ROOT` | **由插件在命令内注入**;"我在哪个盒里"以它们为准,永远不要用工作区路径去推导盒名。 |
|
|
134
136
|
|
|
135
137
|
诊断文件:上述日志目录下的 `provider.log`、`host.log`、`tool.log`。`host.log` 上限 512 KB,只记状态迁移。
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find where DeepSeek Harness itself is installed.
|
|
3
|
+
*
|
|
4
|
+
* `koffi.node` does not come from npm: it ships *inside* the DSH installation,
|
|
5
|
+
* as an unpacked native addon under `resources\app.asar.unpacked`. The launcher
|
|
6
|
+
* that needs it is a plain Node process, so it has no `process.resourcesPath`
|
|
7
|
+
* and no idea where the app that started it lives. `lib/koffi.mjs` used to
|
|
8
|
+
* assume `%LOCALAPPDATA%\Programs\DeepSeek Harness` (and the two `%ProgramFiles%`
|
|
9
|
+
* spellings), which fails the moment DSH is installed anywhere else — exactly
|
|
10
|
+
* like the Sandboxie folder assumption this plugin already had to drop.
|
|
11
|
+
*
|
|
12
|
+
* Four sources, most trustworthy first:
|
|
13
|
+
*
|
|
14
|
+
* 1. `process.resourcesPath` — present when this runs inside Electron; the
|
|
15
|
+
* direct answer.
|
|
16
|
+
* 2. `process.execPath` walked upwards. The host picks the launcher's Node
|
|
17
|
+
* through `redirect.mjs`'s `resolveNodePath()`, which prefers the runtime
|
|
18
|
+
* shipped *inside* the app; when that is what is running, its own path
|
|
19
|
+
* points straight back at the installation.
|
|
20
|
+
* 3. The uninstall entry the installer writes. It lives under a generated key
|
|
21
|
+
* (`…\Uninstall\<GUID>`), so it is found by searching that hive for the
|
|
22
|
+
* display name, and `InstallLocation` / `DisplayIcon` / `UninstallString`
|
|
23
|
+
* are all accepted as evidence — the per-user installer only guarantees
|
|
24
|
+
* some of them.
|
|
25
|
+
* 4. The conventional folders and `PATH`, for an installation that was
|
|
26
|
+
* unpacked by hand.
|
|
27
|
+
*
|
|
28
|
+
* WHY THIS MODULE IMPORTS ONLY NODE BUILTINS. `lib/koffi.mjs` imports it, and
|
|
29
|
+
* `lib/sbie-locate.mjs` imports `lib/koffi.mjs`; reaching back into either from
|
|
30
|
+
* here would close a cycle through the very loader this module exists to feed.
|
|
31
|
+
* The few small helpers it needs are therefore repeated rather than shared, and
|
|
32
|
+
* every side effect is injectable so the whole resolver is testable offline.
|
|
33
|
+
*/
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import fs from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
|
|
38
|
+
/** The executable that names a DSH installation folder. */
|
|
39
|
+
export const DSH_EXE = "DeepSeek Harness.exe";
|
|
40
|
+
/** The name the installer's uninstall entry carries (`DeepSeek Harness 0.2.0-rc.2`). */
|
|
41
|
+
export const DSH_DISPLAY_NAME = "DeepSeek Harness";
|
|
42
|
+
/** The folder name an installation uses by default (`%LOCALAPPDATA%\Programs\DeepSeek Harness`). */
|
|
43
|
+
export const DSH_DIR_NAME = "DeepSeek Harness";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The uninstall hives an installer may have used. A per-user install lands in
|
|
47
|
+
* HKCU; a machine-wide one in HKLM, in the view matching the installer's
|
|
48
|
+
* bitness. Nothing fixed can be queried, because the subkey is a GUID.
|
|
49
|
+
*/
|
|
50
|
+
export const DSH_UNINSTALL_KEYS = [
|
|
51
|
+
"HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
52
|
+
"HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
53
|
+
"HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Environment lookup that is case-insensitive, like Windows' own. */
|
|
57
|
+
function envLookup(env, name) {
|
|
58
|
+
if (env === undefined || env === null) return undefined;
|
|
59
|
+
const wanted = name.toLowerCase();
|
|
60
|
+
for (const key of Object.keys(env)) {
|
|
61
|
+
if (key.toLowerCase() !== wanted) continue;
|
|
62
|
+
const value = env[key];
|
|
63
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Replace `%NAME%` references; unknown names are left alone (they simply fail validation later). */
|
|
69
|
+
export function expandEnvRefs(value, env = process.env) {
|
|
70
|
+
return String(value).replace(/%([^%]+)%/g, (whole, name) => envLookup(env, name) ?? whole);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** De-duplicate case-insensitively, keeping each first occurrence's spelling. */
|
|
74
|
+
export function uniqueDirs(dirs) {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
const unique = [];
|
|
77
|
+
for (const dir of dirs ?? []) {
|
|
78
|
+
if (typeof dir !== "string" || dir.length === 0) continue;
|
|
79
|
+
const key = dir.replace(/[\\/]+$/, "").toLowerCase();
|
|
80
|
+
if (key.length === 0 || seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
unique.push(dir);
|
|
83
|
+
}
|
|
84
|
+
return unique;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function existsWith(exists, candidate) {
|
|
88
|
+
try {
|
|
89
|
+
return exists(candidate) === true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const has = (exists, ...parts) => existsWith(exists, path.join(...parts));
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether `dir` is a DSH installation folder: it holds the app bundle (packed or
|
|
99
|
+
* unpacked) under `resources`, or the launcher executable itself.
|
|
100
|
+
*/
|
|
101
|
+
export function isDshInstallRoot(dir, options = {}) {
|
|
102
|
+
const exists = options.exists ?? fs.existsSync;
|
|
103
|
+
if (typeof dir !== "string" || dir.length === 0) return false;
|
|
104
|
+
return (
|
|
105
|
+
has(exists, dir, "resources", "app.asar") ||
|
|
106
|
+
has(exists, dir, "resources", "app.asar.unpacked") ||
|
|
107
|
+
has(exists, dir, DSH_EXE)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The folders under which the koffi addon may be searched, given either a DSH
|
|
113
|
+
* installation folder or DSH's `resources` folder (or its `app.asar.unpacked`
|
|
114
|
+
* itself). Empty when the candidate is neither — which is what makes an
|
|
115
|
+
* unvalidated guess safe.
|
|
116
|
+
*
|
|
117
|
+
* `app.asar.unpacked` comes first because that is where the addon actually is;
|
|
118
|
+
* the plain `resources` folder is included as well, because the search under it
|
|
119
|
+
* can still reach a payload that lives beside the packed bundle.
|
|
120
|
+
*/
|
|
121
|
+
export function resourcesDirsFor(candidate, options = {}) {
|
|
122
|
+
const exists = options.exists ?? fs.existsSync;
|
|
123
|
+
if (typeof candidate !== "string" || candidate.length === 0) return [];
|
|
124
|
+
const base = path.basename(candidate).toLowerCase();
|
|
125
|
+
|
|
126
|
+
// Already the unpacked payload — which is where the addon actually lives.
|
|
127
|
+
if (base === "app.asar.unpacked") return [candidate];
|
|
128
|
+
|
|
129
|
+
// Already DSH's `resources` folder.
|
|
130
|
+
if (base === "resources" && (has(exists, candidate, "app.asar") || existsWith(exists, path.join(candidate, "app.asar.unpacked")))) {
|
|
131
|
+
return [path.join(candidate, "app.asar.unpacked"), candidate];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// An installation folder — the packed bundle, the unpacked payload or the
|
|
135
|
+
// executable is evidence enough.
|
|
136
|
+
const resources = path.join(candidate, "resources");
|
|
137
|
+
const unpacked = path.join(resources, "app.asar.unpacked");
|
|
138
|
+
if (has(exists, resources, "app.asar") || existsWith(exists, unpacked)) return [unpacked, resources];
|
|
139
|
+
if (has(exists, candidate, DSH_EXE)) return [unpacked, resources];
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Installation folders implied by the interpreter running this code: walk up
|
|
145
|
+
* from `node.exe` (or the Electron binary) and keep every ancestor that looks
|
|
146
|
+
* like a DSH installation. The app ships its Node under
|
|
147
|
+
* `resources\runtime\<pool>\dependencies\node\bin\node.exe`, so this is a
|
|
148
|
+
* pointer, not a guess, whenever the host chose that Node.
|
|
149
|
+
*/
|
|
150
|
+
export function execPathInstallRoots(execPath, options = {}) {
|
|
151
|
+
if (typeof execPath !== "string" || execPath.length === 0) return [];
|
|
152
|
+
const maxUp = options.maxUp ?? 8;
|
|
153
|
+
const roots = [];
|
|
154
|
+
let dir = path.dirname(path.resolve(execPath));
|
|
155
|
+
for (let index = 0; index < maxUp; index += 1) {
|
|
156
|
+
if (isDshInstallRoot(dir, options)) roots.push(dir);
|
|
157
|
+
const parent = path.dirname(dir);
|
|
158
|
+
if (parent === dir) break; // reached the drive root
|
|
159
|
+
dir = parent;
|
|
160
|
+
}
|
|
161
|
+
return roots;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One `reg query` value line: `<name>\t<type>\t<data>`, tabs printed as two or more spaces. */
|
|
165
|
+
export function parseRegValueLine(line) {
|
|
166
|
+
const match = /^\s{2,}(\S.*?)\s{2,}(REG_[A-Z_]+)\s{2,}(.*)$/.exec(String(line));
|
|
167
|
+
if (match === null) return undefined;
|
|
168
|
+
return { name: match[1].trim().toLowerCase(), type: match[2], value: match[3].trim() };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Strip the quotes Windows installers put around paths, and any trailing argument. */
|
|
172
|
+
function unquoteExecutable(value) {
|
|
173
|
+
const text = String(value).trim();
|
|
174
|
+
if (text.startsWith('"')) {
|
|
175
|
+
const end = text.indexOf('"', 1);
|
|
176
|
+
if (end !== -1) return text.slice(1, end);
|
|
177
|
+
}
|
|
178
|
+
return text.replace(/\s+\/\S+$/, "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Installation folders named by an uninstall hive's output.
|
|
183
|
+
*
|
|
184
|
+
* The search prints the whole matching key, so every value of interest is read
|
|
185
|
+
* by name rather than by position. `DisplayIcon` carries `,0` (the icon index)
|
|
186
|
+
* and `UninstallString` carries the uninstaller's switches, so both are reduced
|
|
187
|
+
* to a folder.
|
|
188
|
+
*/
|
|
189
|
+
export function parseUninstallPaths(stdout, env = process.env) {
|
|
190
|
+
const dirs = [];
|
|
191
|
+
for (const line of String(stdout ?? "").split(/\r?\n/)) {
|
|
192
|
+
const parsed = parseRegValueLine(line);
|
|
193
|
+
if (parsed === undefined) continue;
|
|
194
|
+
const value = expandEnvRefs(parsed.value, env);
|
|
195
|
+
if (parsed.name === "installlocation") {
|
|
196
|
+
dirs.push(value.replace(/^"(.*)"$/s, "$1").replace(/[\\/]+$/, ""));
|
|
197
|
+
} else if (parsed.name === "displayicon") {
|
|
198
|
+
const exe = unquoteExecutable(value.replace(/,\s*-?\d+\s*$/, ""));
|
|
199
|
+
if (/[\\/]/.test(exe)) dirs.push(path.dirname(exe));
|
|
200
|
+
} else if (parsed.name === "uninstallstring") {
|
|
201
|
+
const exe = unquoteExecutable(value);
|
|
202
|
+
if (/[\\/]/.test(exe)) dirs.push(path.dirname(exe));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return uniqueDirs(dirs);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Installation folders named by the uninstall hives, through `reg.exe`.
|
|
210
|
+
*
|
|
211
|
+
* One spawn per hive with a data-only search for the display name, which prints
|
|
212
|
+
* only matching keys; parsing needs no block structure because of that. Every
|
|
213
|
+
* failure (no entry, no reg.exe) means "this machine told us nothing", never an
|
|
214
|
+
* error.
|
|
215
|
+
*/
|
|
216
|
+
export function readDshRegistryRoots(options = {}) {
|
|
217
|
+
const env = options.env ?? process.env;
|
|
218
|
+
const spawn = options.spawnSync ?? spawnSync;
|
|
219
|
+
const dirs = [];
|
|
220
|
+
for (const key of DSH_UNINSTALL_KEYS) {
|
|
221
|
+
let result;
|
|
222
|
+
try {
|
|
223
|
+
result = spawn("reg.exe", ["query", key, "/s", "/f", DSH_DISPLAY_NAME, "/d"], {
|
|
224
|
+
encoding: "utf8",
|
|
225
|
+
windowsHide: true,
|
|
226
|
+
timeout: 10000
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (result?.status !== 0) continue;
|
|
232
|
+
dirs.push(...parseUninstallPaths(result.stdout, env));
|
|
233
|
+
}
|
|
234
|
+
return uniqueDirs(dirs);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The folders a DSH installation conventionally occupies. A per-user install
|
|
239
|
+
* goes under `%LOCALAPPDATA%\Programs`; a machine-wide one under either
|
|
240
|
+
* `%ProgramFiles%` spelling, which is why all three are probed.
|
|
241
|
+
*/
|
|
242
|
+
export function defaultDshRoots(env = process.env) {
|
|
243
|
+
const roots = [];
|
|
244
|
+
const localAppData = envLookup(env, "LOCALAPPDATA");
|
|
245
|
+
if (localAppData !== undefined) roots.push(path.join(localAppData, "Programs", DSH_DIR_NAME));
|
|
246
|
+
for (const name of ["ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"]) {
|
|
247
|
+
const base = envLookup(env, name);
|
|
248
|
+
if (base !== undefined) roots.push(path.join(base, DSH_DIR_NAME));
|
|
249
|
+
}
|
|
250
|
+
return roots;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** `PATH` entries that name DeepSeek Harness; a portable install is often launched from one. */
|
|
254
|
+
export function pathDshRoots(env = process.env) {
|
|
255
|
+
const raw = envLookup(env, "PATH");
|
|
256
|
+
if (raw === undefined) return [];
|
|
257
|
+
const roots = [];
|
|
258
|
+
for (const entry of raw.split(";")) {
|
|
259
|
+
const dir = entry.trim().replace(/^"(.*)"$/s, "$1").trim().replace(/[\\/]+$/, "");
|
|
260
|
+
if (dir.length === 0) continue;
|
|
261
|
+
if (!/deepseek[\s_-]*harness/i.test(dir)) continue;
|
|
262
|
+
roots.push(dir);
|
|
263
|
+
}
|
|
264
|
+
return roots;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Every installation folder worth probing, in trust order: what the running
|
|
269
|
+
* interpreter points at, what the installer recorded, then the conventional
|
|
270
|
+
* locations and `PATH`. Later entries are unvalidated on purpose — validation
|
|
271
|
+
* happens in `resourcesDirsFor`, so a wrong guess contributes nothing.
|
|
272
|
+
*/
|
|
273
|
+
export function dshInstallRoots(options = {}) {
|
|
274
|
+
const env = options.env ?? process.env;
|
|
275
|
+
return uniqueDirs([
|
|
276
|
+
...execPathInstallRoots(options.execPath ?? process.execPath, options),
|
|
277
|
+
...(options.registryRoots ?? readDshRegistryRoots(options)),
|
|
278
|
+
...defaultDshRoots(env),
|
|
279
|
+
...pathDshRoots(env)
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The folders to hand to the koffi search, existing ones only, de-duplicated,
|
|
285
|
+
* in trust order.
|
|
286
|
+
*
|
|
287
|
+
* `process.resourcesPath` is consulted first when it exists: inside Electron it
|
|
288
|
+
* IS the answer, and it costs nothing to prefer it.
|
|
289
|
+
*/
|
|
290
|
+
export function dshResourcesRoots(options = {}) {
|
|
291
|
+
const env = options.env ?? process.env;
|
|
292
|
+
const exists = options.exists ?? fs.existsSync;
|
|
293
|
+
const resourcesPath = typeof options.resourcesPath === "string"
|
|
294
|
+
? options.resourcesPath
|
|
295
|
+
: (typeof process.resourcesPath === "string" ? process.resourcesPath : undefined);
|
|
296
|
+
const candidates = [];
|
|
297
|
+
if (resourcesPath !== undefined && resourcesPath.length > 0) candidates.push(resourcesPath);
|
|
298
|
+
candidates.push(...dshInstallRoots({ ...options, env }));
|
|
299
|
+
const dirs = [];
|
|
300
|
+
for (const candidate of candidates) dirs.push(...resourcesDirsFor(candidate, { exists }));
|
|
301
|
+
return uniqueDirs(dirs).filter((dir) => existsWith(exists, dir));
|
|
302
|
+
}
|
package/lib/koffi.mjs
CHANGED
|
@@ -5,34 +5,56 @@
|
|
|
5
5
|
* the harness install path differs per machine and per version, and the plain
|
|
6
6
|
* `koffi` JS wrapper package is not always present. The addon itself is
|
|
7
7
|
* self-contained, so we require the `.node` file directly.
|
|
8
|
+
*
|
|
9
|
+
* WHERE DSH ITSELF IS. The addon ships *inside* the installation, and this
|
|
10
|
+
* module runs in the launcher — a plain Node process with no
|
|
11
|
+
* `process.resourcesPath` — so the folder has to be discovered rather than
|
|
12
|
+
* known. `lib/dsh-locate.mjs` does that: the interpreter's own path walked
|
|
13
|
+
* upwards, the installer's uninstall entry, then the conventional locations and
|
|
14
|
+
* `PATH`. Hardcoding `%LOCALAPPDATA%\Programs\DeepSeek Harness` here is what
|
|
15
|
+
* made a relocated DSH report "koffi.node not found".
|
|
8
16
|
*/
|
|
9
17
|
import { createRequire } from "node:module";
|
|
10
18
|
import fs from "node:fs";
|
|
11
19
|
import path from "node:path";
|
|
12
20
|
import os from "node:os";
|
|
13
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { dshResourcesRoots } from "./dsh-locate.mjs";
|
|
14
23
|
|
|
15
24
|
const require = createRequire(import.meta.url);
|
|
16
25
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
26
|
|
|
18
27
|
let cached;
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The directories the addon search starts from, most specific first: an
|
|
31
|
+
* explicit override, a `vendor` folder beside the package, the DSH installation
|
|
32
|
+
* this process belongs to, then the managed runtime pool.
|
|
33
|
+
*
|
|
34
|
+
* Everything is injectable so the ordering can be tested offline; only the
|
|
35
|
+
* final directory check touches the real filesystem.
|
|
36
|
+
*/
|
|
37
|
+
export function koffiRootCandidates(options = {}) {
|
|
38
|
+
const env = options.env ?? process.env;
|
|
39
|
+
const isDirectory = options.isDirectory ?? ((candidate) => {
|
|
40
|
+
try {
|
|
41
|
+
return fs.statSync(candidate).isDirectory();
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
21
46
|
const roots = [];
|
|
22
47
|
const push = (p) => {
|
|
23
48
|
if (typeof p === "string" && p.length > 0 && !roots.includes(p)) roots.push(p);
|
|
24
49
|
};
|
|
25
|
-
push(
|
|
50
|
+
push(env.DSH_SBIE_KOFFI_ROOT);
|
|
26
51
|
push(path.join(here, "..", "vendor"));
|
|
27
|
-
|
|
28
|
-
push(
|
|
29
|
-
push(
|
|
30
|
-
|
|
31
|
-
push(process.env.DSH_HOME && path.join(process.env.DSH_HOME, "dsh-runtimes"));
|
|
32
|
-
push(path.join(os.homedir(), ".dsh", "dsh-runtimes"));
|
|
33
|
-
return roots.filter((r) => {
|
|
52
|
+
for (const dir of dshResourcesRoots({ ...options, env })) push(dir);
|
|
53
|
+
push(env.DSH_HOME && path.join(env.DSH_HOME, "dsh-runtimes"));
|
|
54
|
+
push(path.join(options.homeDir ?? os.homedir(), ".dsh", "dsh-runtimes"));
|
|
55
|
+
return roots.filter((root) => {
|
|
34
56
|
try {
|
|
35
|
-
return
|
|
57
|
+
return isDirectory(root) === true;
|
|
36
58
|
} catch {
|
|
37
59
|
return false;
|
|
38
60
|
}
|
|
@@ -70,7 +92,7 @@ function findKoffiNode(root, maxDepth = 9) {
|
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
/** Resolve and load koffi once. Throws with an actionable message when absent. */
|
|
73
|
-
export function loadKoffi() {
|
|
95
|
+
export function loadKoffi(options = {}) {
|
|
74
96
|
if (cached !== undefined) return cached;
|
|
75
97
|
if (process.env.DSH_SBIE_KOFFI) {
|
|
76
98
|
cached = require(process.env.DSH_SBIE_KOFFI);
|
|
@@ -81,7 +103,7 @@ export function loadKoffi() {
|
|
|
81
103
|
return cached;
|
|
82
104
|
} catch {}
|
|
83
105
|
const tried = [];
|
|
84
|
-
for (const root of
|
|
106
|
+
for (const root of koffiRootCandidates(options)) {
|
|
85
107
|
tried.push(root);
|
|
86
108
|
const found = findKoffiNode(root);
|
|
87
109
|
if (found !== undefined) {
|
|
@@ -90,6 +112,6 @@ export function loadKoffi() {
|
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
114
|
throw new Error(
|
|
93
|
-
`koffi native addon not found.
|
|
115
|
+
`koffi native addon not found. It ships inside the DeepSeek Harness installation (resources\\app.asar.unpacked), so this usually means DSH could not be located: set DSH_SBIE_KOFFI to a koffi.node path, or DSH_SBIE_KOFFI_ROOT to a folder to search. Searched: ${tried.join(", ") || "(no candidate roots)"}`
|
|
94
116
|
);
|
|
95
117
|
}
|
package/lib/tool.mjs
CHANGED
|
@@ -220,6 +220,13 @@ export async function apply(ctx) {
|
|
|
220
220
|
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
221
221
|
throw new Error("sandbox_clear needs the calling session's workspace, and this caller has none");
|
|
222
222
|
}
|
|
223
|
+
// Declared before ANY use. It used to sit below the `status` early
|
|
224
|
+
// return, inside the same function body — a temporal dead zone, so
|
|
225
|
+
// `mode: "status"` threw `ReferenceError: Cannot access 'workspace'
|
|
226
|
+
// before initialization` on every single call. test/tool-execute.mjs
|
|
227
|
+
// drives all three modes, so this class of failure is now a failed
|
|
228
|
+
// test rather than a bug report from a user.
|
|
229
|
+
const workspace = naming.canonicalWorkspace(cwd);
|
|
223
230
|
if (args.mode === "status") {
|
|
224
231
|
// Ask the projection registry what it holds for THIS session. The
|
|
225
232
|
// header chip reads that value, so this separates "the fold never
|
|
@@ -236,7 +243,8 @@ export async function apply(ctx) {
|
|
|
236
243
|
}
|
|
237
244
|
log(`status: ${detail}`);
|
|
238
245
|
return Promise.resolve({ action: "status", workspace, copies: [], cleared: [], note: detail });
|
|
239
|
-
}
|
|
246
|
+
}
|
|
247
|
+
|
|
240
248
|
const before = copiesFor(host, naming, workspace);
|
|
241
249
|
if (before === null) {
|
|
242
250
|
throw new Error(`cannot read ${host.sandboxIniPath()}, so the sandbox state is unknown; refusing to act`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lone-wolf-akela/dsh-sandboxie-redirect",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "DSH bundle adding a copy-on-write permission preset: shell commands run inside a per-workspace Sandboxie box, so writes outside the workspace land in the box's copy while the real disk is untouched.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"./package.json": "./package.json"
|
|
43
43
|
},
|
|
44
44
|
"bin": {
|
|
45
|
-
"dsh-sbie-run": "
|
|
45
|
+
"dsh-sbie-run": "bin/dsh-sbie-run.mjs"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"lib",
|
|
@@ -76,10 +76,11 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
|
-
"test": "node test/unit.mjs && node test/sbie-locate.mjs && node test/host-projection.mjs && node test/validate-patch.mjs",
|
|
79
|
+
"test": "node test/unit.mjs && node test/sbie-locate.mjs && node test/dsh-locate.mjs && node test/tool-execute.mjs && node test/host-projection.mjs && node test/validate-patch.mjs",
|
|
80
80
|
"test:acceptance": "node test/run-tests.mjs",
|
|
81
81
|
"test:soak": "node test/soak.mjs 30",
|
|
82
|
-
"test:locate": "node test/sbie-locate.mjs",
|
|
82
|
+
"test:locate": "node test/sbie-locate.mjs && node test/dsh-locate.mjs",
|
|
83
|
+
"test:tool": "node test/tool-execute.mjs",
|
|
83
84
|
"validate:patch": "node test/validate-patch.mjs",
|
|
84
85
|
"pack:check": "npm pack --dry-run"
|
|
85
86
|
},
|