@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 +74 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/README.zh.md +150 -0
- package/bin/dsh-sbie-run.mjs +23 -0
- package/cordis.patch.yml +79 -0
- package/lib/boxes.mjs +220 -0
- package/lib/cli.mjs +164 -0
- package/lib/client.js +299 -0
- package/lib/core.mjs +59 -0
- package/lib/host.mjs +356 -0
- package/lib/inner.mjs +116 -0
- package/lib/koffi.mjs +95 -0
- package/lib/log.mjs +62 -0
- package/lib/naming.mjs +159 -0
- package/lib/note.mjs +43 -0
- package/lib/protocol.mjs +63 -0
- package/lib/provider.mjs +210 -0
- package/lib/redirect.mjs +167 -0
- package/lib/run.mjs +343 -0
- package/lib/sbie.mjs +268 -0
- package/lib/tool-schema.mjs +109 -0
- package/lib/tool.mjs +323 -0
- package/lib/words.mjs +46 -0
- package/package.json +94 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sandbox_clear`'s output schema, kept out of the tool definition so a test can
|
|
3
|
+
* check it WITHOUT booting DSH.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists: `defineTool` compiles `parameters` with the PARAMETER
|
|
6
|
+
* spec DSL and `output.schema` with the VALUE schema DSL, and the two do not
|
|
7
|
+
* agree about `required`. The value DSL allows it only on a PROPERTY node — the
|
|
8
|
+
* direct children of a `properties` map — while the root, an `items` object and
|
|
9
|
+
* a `oneOf` branch all reject it. Getting that wrong does not merely lose the
|
|
10
|
+
* tool: `defineTool` throws while the plugin row composes, and the throw happens
|
|
11
|
+
* inside a cordis callback, where the loader reports it as an unattributed
|
|
12
|
+
* failure. That is exactly how a schema typo turned into "the tool silently does
|
|
13
|
+
* not exist" and, one restart later, into a boot failure.
|
|
14
|
+
*
|
|
15
|
+
* The rules below mirror `runSchemaCompiler`/`allowRequired` in
|
|
16
|
+
* `@deepseek-ai/dsh-tools` (read from the shipped bundle, not guessed):
|
|
17
|
+
* - every node: the annotation keys, plus `type` (or `oneOf`), plus `enum` and
|
|
18
|
+
* `const` on scalars, plus `properties`/`additionalProperties` on objects and
|
|
19
|
+
* `items` on arrays;
|
|
20
|
+
* - `required` only where the parent node is a property-map entry;
|
|
21
|
+
* - every OBJECT node must state `additionalProperties` as a boolean.
|
|
22
|
+
* The authoritative compiler still runs at registration; this is a pre-flight so
|
|
23
|
+
* a bad schema is caught before it costs a restart.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Author-facing keys the DSL accepts on any node. */
|
|
27
|
+
const ANNOTATION_KEYS = ["description", "title", "default", "examples"];
|
|
28
|
+
/** Keys accepted on scalar nodes. */
|
|
29
|
+
const SCALAR_TYPES = ["string", "number", "integer", "boolean", "null"];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect every way `schema` breaks the value schema DSL.
|
|
33
|
+
* @param schema - an object-rooted value schema.
|
|
34
|
+
* @param path - diagnostic path for this node.
|
|
35
|
+
* @param allowRequired - whether this node may carry `required` (only property-map entries may).
|
|
36
|
+
* @returns a list of human-readable violations; empty means it looks legal.
|
|
37
|
+
*/
|
|
38
|
+
export function valueSchemaViolations(schema, path = "output.schema", allowRequired = false) {
|
|
39
|
+
const problems = [];
|
|
40
|
+
if (typeof schema !== "object" || schema === null) return [`${path} must be an object`];
|
|
41
|
+
const keys = Object.keys(schema);
|
|
42
|
+
const base = [...ANNOTATION_KEYS, "type", ...(allowRequired ? ["required"] : [])];
|
|
43
|
+
|
|
44
|
+
if (Object.hasOwn(schema, "oneOf")) {
|
|
45
|
+
const extra = keys.filter((key) => ![...base, "oneOf"].includes(key));
|
|
46
|
+
for (const key of extra) problems.push(`${path}.${key} is not a valid key on a oneOf node`);
|
|
47
|
+
if (Object.hasOwn(schema, "type")) problems.push(`${path} cannot declare both type and oneOf`);
|
|
48
|
+
if (!Array.isArray(schema.oneOf) || schema.oneOf.length < 2) problems.push(`${path}.oneOf must hold at least two schemas`);
|
|
49
|
+
else schema.oneOf.forEach((branch, index) => problems.push(...valueSchemaViolations(branch, `${path}.oneOf[${index}]`, false)));
|
|
50
|
+
return problems;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const type = schema.type;
|
|
54
|
+
if (!SCALAR_TYPES.includes(type) && type !== "object" && type !== "array" && type !== "json") {
|
|
55
|
+
problems.push(`${path}.type must be string/number/integer/boolean/null/array/object/json, or use oneOf`);
|
|
56
|
+
return problems;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const allowed =
|
|
60
|
+
type === "object"
|
|
61
|
+
? [...base, "properties", "additionalProperties"]
|
|
62
|
+
: type === "array"
|
|
63
|
+
? [...base, "items"]
|
|
64
|
+
: type === "json"
|
|
65
|
+
? [...base]
|
|
66
|
+
: [...base, "enum", "const"];
|
|
67
|
+
for (const key of keys) if (!allowed.includes(key)) problems.push(`${path}.${key} is not supported by the value schema DSL${key === "required" ? " on this node (only a property-map entry may carry it)" : ""}`);
|
|
68
|
+
|
|
69
|
+
if (type === "object") {
|
|
70
|
+
if (typeof schema.additionalProperties !== "boolean") problems.push(`${path}.additionalProperties must be explicitly true or false`);
|
|
71
|
+
for (const [name, child] of Object.entries(schema.properties ?? {})) {
|
|
72
|
+
// Property-map entries are the ONLY nodes that may carry `required`.
|
|
73
|
+
problems.push(...valueSchemaViolations(child, `${path}.properties.${name}`, true));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (type === "array" && Object.hasOwn(schema, "items")) {
|
|
77
|
+
problems.push(...valueSchemaViolations(schema.items, `${path}.items`, false));
|
|
78
|
+
}
|
|
79
|
+
return problems;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Wire payload of `sandbox_clear`: what it did, and what it found. */
|
|
83
|
+
export const OUTPUT_SCHEMA = {
|
|
84
|
+
type: "object",
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
properties: {
|
|
87
|
+
action: { type: "string", required: true, enum: ["inspect", "clear", "status"] },
|
|
88
|
+
workspace: { type: "string", required: true },
|
|
89
|
+
copies: {
|
|
90
|
+
type: "array",
|
|
91
|
+
required: true,
|
|
92
|
+
items: {
|
|
93
|
+
// No `required` on this node: an `items` object is not a property-map
|
|
94
|
+
// entry, so the DSL rejects it there (see the header).
|
|
95
|
+
type: "object",
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
properties: {
|
|
98
|
+
box: { type: "string", required: true },
|
|
99
|
+
root: { type: "string", required: true },
|
|
100
|
+
files: { type: "integer", required: true },
|
|
101
|
+
bytes: { type: "integer", required: true },
|
|
102
|
+
truncated: { type: "boolean", required: true }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
cleared: { type: "array", required: true, items: { type: "string" } },
|
|
107
|
+
note: { type: "string", required: true }
|
|
108
|
+
}
|
|
109
|
+
};
|
package/lib/tool.mjs
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sandbox_clear` tool — let the agent discard this workspace's Sandboxie
|
|
3
|
+
* copy without leaving the conversation.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* Under the copy-on-write preset, reads and writes outside the workspace are
|
|
8
|
+
* served from a per-workspace copy. That is the point of the mode, but in a long
|
|
9
|
+
* task it creates two problems a shell command cannot fix:
|
|
10
|
+
*
|
|
11
|
+
* - a STALE copy from an earlier run masks the real state (a file "exists" that
|
|
12
|
+
* the real disk does not have, or an old virtualised edit is still in force);
|
|
13
|
+
* - a copy that only grows (every `%TEMP%` write, every download) hides the
|
|
14
|
+
* real filesystem behind it.
|
|
15
|
+
*
|
|
16
|
+
* Discarding the copy needs Sandboxie's own API, which a BOXED process cannot
|
|
17
|
+
* reach (Sandboxie hides the box list from one), so a shell command cannot do
|
|
18
|
+
* it. The harness process can, which is also why this tool works in every
|
|
19
|
+
* permission mode: the mode governs how commands are spawned, not what a
|
|
20
|
+
* host-side tool may do.
|
|
21
|
+
*
|
|
22
|
+
* WHY IT LOOKS LIKE THIS (a real failure, not a preference)
|
|
23
|
+
* --------------------------------------------------------
|
|
24
|
+
* The first version imported `boxes.mjs`, which loads koffi — a NATIVE module.
|
|
25
|
+
* The launcher is a plain-Node process where koffi loads fine (that is how
|
|
26
|
+
* `--manage` has always worked), but this half runs inside the Electron host,
|
|
27
|
+
* and the row never registered: no tool, and no error anyone could see, because
|
|
28
|
+
* a module-level import failure happens before any of this file's code can log.
|
|
29
|
+
* So now:
|
|
30
|
+
*
|
|
31
|
+
* - the module level imports NOTHING but Node builtins, so every failure below
|
|
32
|
+
* is reached and logged to `tool.log`;
|
|
33
|
+
* - native work happens in a CHILD process running the launcher under the
|
|
34
|
+
* managed Node runtime — the exact path `--manage clean` already uses;
|
|
35
|
+
* - the ownership question is answered with pure fs (the same helpers the
|
|
36
|
+
* header chip uses), so no native module is needed to decide what is ours.
|
|
37
|
+
*
|
|
38
|
+
* SAFETY
|
|
39
|
+
* ------
|
|
40
|
+
* - It never takes a box name or a path from the model. The only boxes it can
|
|
41
|
+
* touch are those whose Sandboxie configuration grants THIS session's
|
|
42
|
+
* workspace access — the same test the chip uses, so what the tool reports
|
|
43
|
+
* and what the user sees cannot disagree.
|
|
44
|
+
* - `mode: "clear"` is explicit and is the only thing that deletes; `inspect`
|
|
45
|
+
* is a pure read, including the size accounting. Both exist because anything
|
|
46
|
+
* living only in the copy is destroyed by a clear.
|
|
47
|
+
*/
|
|
48
|
+
import { spawnSync } from "node:child_process";
|
|
49
|
+
import fs from "node:fs";
|
|
50
|
+
import path from "node:path";
|
|
51
|
+
import { fileURLToPath } from "node:url";
|
|
52
|
+
import { logFile } from "./log.mjs";
|
|
53
|
+
import { resolveNodePath } from "./redirect.mjs";
|
|
54
|
+
|
|
55
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const PACKAGE_ROOT = path.resolve(HERE, "..");
|
|
57
|
+
/** Diagnostics land in the plugin's state directory, never inside the package. */
|
|
58
|
+
const LOG_PATH = logFile("tool.log");
|
|
59
|
+
|
|
60
|
+
/** Stop accounting after this many files or this long, and say so. */
|
|
61
|
+
const WALK_FILE_CAP = 20000;
|
|
62
|
+
const WALK_MS_CAP = 3000;
|
|
63
|
+
|
|
64
|
+
/** Append one diagnostic line; a diagnostic must never be the reason a call fails. */
|
|
65
|
+
function log(message) {
|
|
66
|
+
try {
|
|
67
|
+
fs.appendFileSync(LOG_PATH, `${new Date().toISOString()} ${message}\n`);
|
|
68
|
+
} catch {
|
|
69
|
+
/* nothing sensible to do here */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The registrant context, kept for `mode: "status"`. Null outside a live plugin.
|
|
75
|
+
* @type {any}
|
|
76
|
+
*/
|
|
77
|
+
let pluginScope = null;
|
|
78
|
+
|
|
79
|
+
/** Size up one copy tree. Host-side on purpose: walking `C:\Sandbox\…` from inside the box hangs. */
|
|
80
|
+
function measure(root) {
|
|
81
|
+
let files = 0;
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let truncated = false;
|
|
84
|
+
if (!fs.existsSync(root)) return { files, bytes, truncated };
|
|
85
|
+
const started = Date.now();
|
|
86
|
+
const stack = [root];
|
|
87
|
+
while (stack.length > 0) {
|
|
88
|
+
if (files >= WALK_FILE_CAP || Date.now() - started > WALK_MS_CAP) {
|
|
89
|
+
truncated = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const dir = stack.pop();
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const full = path.join(dir, entry.name);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
stack.push(full);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!entry.isFile()) continue;
|
|
106
|
+
files += 1;
|
|
107
|
+
try {
|
|
108
|
+
bytes += fs.statSync(full).size;
|
|
109
|
+
} catch {
|
|
110
|
+
/* a file that vanished mid-walk simply does not count */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { files, bytes, truncated };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every box whose Sandboxie configuration grants this workspace access, with its
|
|
119
|
+
* size. Pure fs — no Sandboxie API, no native module. Exported so a test (and
|
|
120
|
+
* the `inspect-copies` CLI) can run it against the real configuration.
|
|
121
|
+
* @param host - the loaded host half (its config helpers).
|
|
122
|
+
* @param naming - the loaded naming helpers (`boxRoot`).
|
|
123
|
+
* @param workspace - the session workspace.
|
|
124
|
+
* @param options.sizes - pass false to skip the walk; the caller is then running
|
|
125
|
+
* INSIDE a box, where measuring `C:\Sandbox\…` recurses into the redirection
|
|
126
|
+
* layer and hangs.
|
|
127
|
+
*/
|
|
128
|
+
export function copiesFor(host, naming, workspace, options = {}) {
|
|
129
|
+
const sections = host.readIniSections();
|
|
130
|
+
if (sections === null) return null;
|
|
131
|
+
const copies = [];
|
|
132
|
+
for (const [box, body] of sections) {
|
|
133
|
+
if (!host.sectionOwnsWorkspace(body, workspace)) continue;
|
|
134
|
+
const root = host.rootForBox(box, sections);
|
|
135
|
+
copies.push(options.sizes === false ? { box, root } : { box, root, ...measure(root) });
|
|
136
|
+
}
|
|
137
|
+
return copies;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function humanBytes(bytes) {
|
|
141
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
142
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
143
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
144
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const DESCRIPTION = [
|
|
148
|
+
"Discard this workspace's Sandboxie copy — the redirected writes the copy-on-write permission preset collects for paths OUTSIDE the workspace.",
|
|
149
|
+
"Use it when a command's view of the filesystem disagrees with the real disk (a file that exists only in the copy, or a stale copy from an earlier run masking the real state), or to reclaim the space a long task accumulated.",
|
|
150
|
+
'`mode: "inspect"` only reports; `mode: "clear"` stops the box\'s processes, deletes its copy and drops its Sandboxie configuration, so the next copy-on-write command starts from a fresh empty box. Anything that exists ONLY in the copy is destroyed, so inspect first when the copy may hold the only copy of something.',
|
|
151
|
+
"Only boxes whose Sandboxie configuration grants THIS session's workspace access can be touched, and the tool works in every permission mode because it runs in the harness process rather than in the sandbox."
|
|
152
|
+
].join(" ");
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Register `sandbox_clear`.
|
|
156
|
+
* @param ctx - registrant context carrying the tool registry.
|
|
157
|
+
*/
|
|
158
|
+
export async function apply(ctx) {
|
|
159
|
+
try {
|
|
160
|
+
// Everything risky is imported HERE, not at module level: a throw above this
|
|
161
|
+
// point aborts the row before any log line can be written, which is exactly
|
|
162
|
+
// how the first version failed invisibly.
|
|
163
|
+
const { importCore } = await import("./core.mjs");
|
|
164
|
+
const { defineTool } = await importCore("@deepseek-ai/dsh-tools");
|
|
165
|
+
const { OUTPUT_SCHEMA, valueSchemaViolations } = await import("./tool-schema.mjs");
|
|
166
|
+
const host = await import("./host.mjs");
|
|
167
|
+
const naming = await import("./naming.mjs");
|
|
168
|
+
|
|
169
|
+
ctx.inject(["tools"], (scope) => {
|
|
170
|
+
// cordis CALLS this callback, so it is outside the try/catch below: a
|
|
171
|
+
// throw here is reported by the loader as an unattributed failure, which
|
|
172
|
+
// is how the previous schema error stayed invisible in the logs. Hence its
|
|
173
|
+
// own guard.
|
|
174
|
+
try {
|
|
175
|
+
// Kept so `mode: "status"` can ask the projection registry directly what
|
|
176
|
+
// it holds for the calling session. The header chip goes through that
|
|
177
|
+
// registry, and when the chip is wrong there is otherwise no way to tell a
|
|
178
|
+
// stale client from a fold that never ran.
|
|
179
|
+
pluginScope = scope;
|
|
180
|
+
// Pre-flight the schema, because `defineTool` throwing here is invisible
|
|
181
|
+
// and cost two restarts. The authoritative compiler still has the last
|
|
182
|
+
// word; this converts an unattributed throw into a logged skip.
|
|
183
|
+
const problems = valueSchemaViolations(OUTPUT_SCHEMA);
|
|
184
|
+
if (problems.length > 0) {
|
|
185
|
+
for (const problem of problems) log(`schema pre-flight: ${problem}`);
|
|
186
|
+
log("refusing to register sandbox_clear with an invalid output schema");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
scope.tools.register(
|
|
190
|
+
defineTool({
|
|
191
|
+
name: "sandbox_clear",
|
|
192
|
+
description: DESCRIPTION,
|
|
193
|
+
parameters: {
|
|
194
|
+
mode: {
|
|
195
|
+
type: "string",
|
|
196
|
+
required: true,
|
|
197
|
+
enum: ["inspect", "clear", "status"],
|
|
198
|
+
description: "inspect reports the copies and their size; clear deletes them (destroying anything that exists only in the copy)."
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
output: {
|
|
202
|
+
// Checked by valueSchemaViolations() before registration, and by
|
|
203
|
+
// test/validate-patch.mjs offline: see lib/tool-schema.mjs for why the
|
|
204
|
+
// parameter DSL and the value schema DSL disagree about required.
|
|
205
|
+
schema: OUTPUT_SCHEMA,
|
|
206
|
+
render: (_args, value) => {
|
|
207
|
+
const lines = value.copies.map(
|
|
208
|
+
(copy) =>
|
|
209
|
+
`- ${copy.box}: ${copy.files} file(s), ${humanBytes(copy.bytes)}${copy.truncated ? " (counting stopped early)" : ""} at ${copy.root}`
|
|
210
|
+
);
|
|
211
|
+
const head =
|
|
212
|
+
value.action === "clear"
|
|
213
|
+
? `Cleared ${value.cleared.length} sandbox copy/copies for ${value.workspace}.`
|
|
214
|
+
: `Sandbox copies for ${value.workspace}: ${value.copies.length} found.`;
|
|
215
|
+
return [{ type: "text", text: [head, ...lines, value.note].join("\n") }];
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
execute(args, exec) {
|
|
219
|
+
const cwd = exec.agent?.session?.header?.cwd;
|
|
220
|
+
if (typeof cwd !== "string" || cwd.length === 0) {
|
|
221
|
+
throw new Error("sandbox_clear needs the calling session's workspace, and this caller has none");
|
|
222
|
+
}
|
|
223
|
+
if (args.mode === "status") {
|
|
224
|
+
// Ask the projection registry what it holds for THIS session. The
|
|
225
|
+
// header chip reads that value, so this separates "the fold never
|
|
226
|
+
// ran" from "the client never got it" — the ambiguity that made the
|
|
227
|
+
// chip cost several restarts to even locate.
|
|
228
|
+
let detail;
|
|
229
|
+
try {
|
|
230
|
+
const projections = pluginScope?.get?.("sessionProjections");
|
|
231
|
+
const session = exec.agent?.session;
|
|
232
|
+
const state = projections?.stateOf?.(session, "sandboxBox");
|
|
233
|
+
detail = `projections=${projections === undefined ? "absent" : "present"} session=${session?.id ?? "?"} seq=${session?.seq ?? "?"} stateOf=${JSON.stringify(state ?? null)}`;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
detail = `status query threw: ${error?.message ?? error}`;
|
|
236
|
+
}
|
|
237
|
+
log(`status: ${detail}`);
|
|
238
|
+
return Promise.resolve({ action: "status", workspace, copies: [], cleared: [], note: detail });
|
|
239
|
+
} const workspace = naming.canonicalWorkspace(cwd);
|
|
240
|
+
const before = copiesFor(host, naming, workspace);
|
|
241
|
+
if (before === null) {
|
|
242
|
+
throw new Error(`cannot read ${host.sandboxIniPath()}, so the sandbox state is unknown; refusing to act`);
|
|
243
|
+
}
|
|
244
|
+
const listed = before.map((copy) => ({
|
|
245
|
+
box: copy.box,
|
|
246
|
+
root: copy.root,
|
|
247
|
+
files: copy.files,
|
|
248
|
+
bytes: copy.bytes,
|
|
249
|
+
truncated: copy.truncated
|
|
250
|
+
}));
|
|
251
|
+
log(`execute mode=${args.mode} workspace=${workspace} copies=${listed.map((copy) => copy.box).join(",") || "(none)"}`);
|
|
252
|
+
|
|
253
|
+
if (args.mode === "inspect") {
|
|
254
|
+
return Promise.resolve({
|
|
255
|
+
action: "inspect",
|
|
256
|
+
workspace,
|
|
257
|
+
copies: listed,
|
|
258
|
+
cleared: [],
|
|
259
|
+
note:
|
|
260
|
+
before.length === 0
|
|
261
|
+
? "No copy exists for this workspace, so reads and writes outside it already reflect the real disk."
|
|
262
|
+
: 'Call again with mode: "clear" to delete these copies. Files that exist only in a copy are lost.'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The deletion runs in a CHILD process under the managed Node runtime:
|
|
267
|
+
// the launcher owns the Sandboxie/native work, and this is the same
|
|
268
|
+
// `--manage clean` path a human would use from a console.
|
|
269
|
+
const node = resolveNodePath();
|
|
270
|
+
if (node === null) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"cannot find a genuine Node runtime to run the launcher; set DSH_SBIE_NODE to a node.exe path"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const launcher = path.join(PACKAGE_ROOT, "bin", "dsh-sbie-run.mjs");
|
|
276
|
+
const result = spawnSync(node, [launcher, "--manage", "clean", workspace], {
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
timeout: 180000
|
|
280
|
+
});
|
|
281
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
282
|
+
log(`clear exit=${result.status} output=${output.replace(/\s+/g, " ").slice(0, 400)}`);
|
|
283
|
+
// The box's existence is what the header chip and the session
|
|
284
|
+
// projection read, so drop the memo now instead of waiting it out.
|
|
285
|
+
host.invalidateBoxCache();
|
|
286
|
+
const stillThere = copiesFor(host, naming, workspace) ?? [];
|
|
287
|
+
const cleared = listed.filter((copy) => !stillThere.some((after) => after.box === copy.box)).map((copy) => copy.box);
|
|
288
|
+
|
|
289
|
+
if (result.status !== 0 && cleared.length === 0) {
|
|
290
|
+
throw new Error(`the launcher could not clear the copies (exit ${result.status}): ${output || "no output"}`);
|
|
291
|
+
}
|
|
292
|
+
return Promise.resolve({
|
|
293
|
+
action: "clear",
|
|
294
|
+
workspace,
|
|
295
|
+
copies: listed,
|
|
296
|
+
cleared,
|
|
297
|
+
note:
|
|
298
|
+
listed.length === 0
|
|
299
|
+
? "Nothing to clear: no managed box belongs to this workspace."
|
|
300
|
+
: "The copies are gone, so paths outside the workspace read from the real disk again. The next copy-on-write command creates a fresh, empty box."
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
presentCall: (args) => ({
|
|
304
|
+
card: "generic",
|
|
305
|
+
title: args.mode === "clear" ? "Clear the sandbox copy" : "Inspect the sandbox copy",
|
|
306
|
+
kind: "other",
|
|
307
|
+
rawInput: args
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
log("apply: registered sandbox_clear");
|
|
312
|
+
} catch (error) {
|
|
313
|
+
log(`registering sandbox_clear FAILED: ${error?.stack ?? error}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
// `apply` is async, so an escaping throw would surface as an unhandled
|
|
318
|
+
// rejection instead of a readable diagnostic.
|
|
319
|
+
log(`apply FAILED: ${error?.stack ?? error}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const inject = ["tools"];
|
package/lib/words.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word lists for Docker-style box names (`dsh_brisk_otter`).
|
|
3
|
+
*
|
|
4
|
+
* Constraints that shaped these lists:
|
|
5
|
+
* - Sandboxie accepts only alphanumerics and `_` in a box name (Start.exe's own
|
|
6
|
+
* parser: `iswalnum(c) || c === '_'`), so every word is plain lowercase ASCII.
|
|
7
|
+
* - The whole name must stay well inside Sandboxie's 34-wide-character box-name
|
|
8
|
+
* buffer, so words are short.
|
|
9
|
+
* - 90 x 90 = 8100 first-choice combinations; `boxNameCandidates` walks further
|
|
10
|
+
* on the rare collision instead of lengthening the name.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const ADJECTIVES = [
|
|
14
|
+
"able", "amber", "ancient", "arctic", "autumn", "azure", "bald", "beige", "bitter", "bold",
|
|
15
|
+
"bouncy", "brave", "breezy", "bright", "brisk", "bronze", "burly", "busy", "calm", "candid",
|
|
16
|
+
"canny", "cheery", "chilly", "civic", "clever", "cloudy", "cosmic", "cozy", "crimson", "crisp",
|
|
17
|
+
"curious", "dapper", "daring", "dawn", "deft", "dense", "dizzy", "dusty", "eager", "early",
|
|
18
|
+
"earthy", "elated", "electric", "elegant", "ember", "fancy", "fearless", "fleet", "fluffy", "foggy",
|
|
19
|
+
"fond", "frank", "frosty", "gilded", "glad", "glossy", "golden", "graceful", "grand", "grateful",
|
|
20
|
+
"green", "hardy", "hazel", "hearty", "hidden", "honest", "humble", "ivory", "jolly", "jovial",
|
|
21
|
+
"keen", "kindly", "lively", "lucid", "lucky", "lush", "mellow", "merry", "mighty", "misty",
|
|
22
|
+
"modest", "mossy", "nimble", "noble", "olive", "placid", "playful", "plucky", "polar", "polished",
|
|
23
|
+
"proud", "quaint", "quiet", "rapid", "rustic", "sandy", "scarlet", "serene", "sharp", "shiny",
|
|
24
|
+
"silent", "silky", "silver", "sleek", "smooth", "snowy", "sober", "solar", "spry", "steady",
|
|
25
|
+
"sturdy", "sunny", "swift", "tame", "tender", "teal", "tidy", "tiny", "topaz", "tranquil",
|
|
26
|
+
"valiant", "velvet", "vivid", "warm", "wise", "witty", "zesty"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const NOUNS = [
|
|
30
|
+
"acorn", "alder", "anchor", "antler", "arrow", "aspen", "atlas", "badger", "bamboo", "basin",
|
|
31
|
+
"beacon", "beaver", "berry", "birch", "bison", "blossom", "bramble", "brook", "bubble", "cactus",
|
|
32
|
+
"canyon", "cedar", "cello", "cinder", "clover", "cobalt", "comet", "compass", "coral", "cosmos",
|
|
33
|
+
"cotton", "crab", "crane", "crater", "cricket", "crystal", "cypress", "dahlia", "daisy", "dolphin",
|
|
34
|
+
"dragon", "dune", "eagle", "ember", "falcon", "fawn", "fern", "finch", "fjord", "flint",
|
|
35
|
+
"forest", "fossil", "fox", "galaxy", "garden", "gecko", "ginger", "glacier", "gopher", "granite",
|
|
36
|
+
"grove", "gull", "harbor", "hazel", "heron", "hickory", "hollow", "ibis", "indigo", "island",
|
|
37
|
+
"ivy", "jasmine", "jasper", "juniper", "kestrel", "koala", "lantern", "larch", "lark", "laurel",
|
|
38
|
+
"lava", "lemon", "lilac", "lily", "linen", "lotus", "lynx", "magnet", "magnolia", "mango",
|
|
39
|
+
"maple", "marble", "meadow", "mesa", "meteor", "mint", "moth", "mulberry", "nebula", "nectar",
|
|
40
|
+
"nettle", "oasis", "ocean", "olive", "onyx", "orbit", "orchid", "osprey", "otter", "panda",
|
|
41
|
+
"pebble", "pelican", "penguin", "pepper", "petal", "pine", "pixel", "plum", "pollen", "poppy",
|
|
42
|
+
"prairie", "prism", "puffin", "quartz", "quill", "raven", "reef", "ridge", "robin", "rowan",
|
|
43
|
+
"saffron", "sage", "salmon", "sequoia", "shale", "sparrow", "spruce", "summit", "sycamore", "talon",
|
|
44
|
+
"thistle", "thunder", "tiger", "timber", "topaz", "tulip", "tundra", "turtle", "valley", "violet",
|
|
45
|
+
"walnut", "walrus", "willow", "winter", "wren", "yarrow", "zephyr"
|
|
46
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lone-wolf-akela/dsh-sandboxie-redirect",
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "lone-wolf-akela",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/lone-wolf-akela/dsh-sandboxie-redirect.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/lone-wolf-akela/dsh-sandboxie-redirect/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/lone-wolf-akela/dsh-sandboxie-redirect#readme",
|
|
19
|
+
"os": [
|
|
20
|
+
"win32"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"main": "lib/provider.mjs",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"default": "./lib/provider.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./provider": {
|
|
31
|
+
"default": "./lib/provider.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./host": {
|
|
34
|
+
"default": "./lib/host.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./note": {
|
|
37
|
+
"default": "./lib/note.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./client": {
|
|
40
|
+
"default": "./lib/client.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"bin": {
|
|
45
|
+
"dsh-sbie-run": "./bin/dsh-sbie-run.mjs"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"lib",
|
|
49
|
+
"bin",
|
|
50
|
+
"cordis.patch.yml",
|
|
51
|
+
"README.md",
|
|
52
|
+
"README.zh.md",
|
|
53
|
+
"CHANGELOG.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"keywords": [
|
|
57
|
+
"dsh",
|
|
58
|
+
"dsh-plugin",
|
|
59
|
+
"dsh-bundle",
|
|
60
|
+
"deepseek-harness",
|
|
61
|
+
"sandboxie",
|
|
62
|
+
"sandbox",
|
|
63
|
+
"copy-on-write",
|
|
64
|
+
"permission-preset",
|
|
65
|
+
"windows"
|
|
66
|
+
],
|
|
67
|
+
"dsh": {
|
|
68
|
+
"bundle": {
|
|
69
|
+
"patch": "./cordis.patch.yml"
|
|
70
|
+
},
|
|
71
|
+
"client": {
|
|
72
|
+
"inject": [
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
74
|
+
],
|
|
75
|
+
"platform": "web"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"test": "node test/unit.mjs && node test/host-projection.mjs && node test/validate-patch.mjs",
|
|
80
|
+
"test:acceptance": "node test/run-tests.mjs",
|
|
81
|
+
"test:soak": "node test/soak.mjs 30",
|
|
82
|
+
"validate:patch": "node test/validate-patch.mjs",
|
|
83
|
+
"pack:check": "npm pack --dry-run"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-sandbox-local": "^0.2.0-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"js-yaml": "^4.1.0"
|
|
93
|
+
}
|
|
94
|
+
}
|