@morlay/dsh-sandbox-local 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -21
- package/dist/client.cjs +0 -15663
- package/dist/client.d.cts +2 -1
- package/dist/client.d.mts +2 -1
- package/dist/client.mjs +34 -0
- package/dist/index.cjs +503 -0
- package/dist/index.d.mts +3 -3
- package/dist/rows.cjs +20 -0
- package/package.json +43 -36
- package/src/client/index.ts +0 -2
- package/dist/index.d.cts +0 -25
- package/dist/rows.d.cts +0 -13
package/dist/client.d.cts
CHANGED
package/dist/client.d.mts
CHANGED
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/client/locales.ts
|
|
2
|
+
const zh = {
|
|
3
|
+
access: "额外可写根与拒绝项",
|
|
4
|
+
accessHint: "一行一条:`rw <路径>` 追加可写根,`r- <路径>` 只读,`-- <路径>` 拒绝;支持环境变量模板与 glob(`rw` 项必须是具体路径)。改完当场生效。"
|
|
5
|
+
};
|
|
6
|
+
const en = {
|
|
7
|
+
access: "Extra writable roots and refusals",
|
|
8
|
+
accessHint: "One entry per line: `rw <path>` grants a writable root, `r- <path>` makes it read-only, `-- <path>` denies it; env templates and globs work (`rw` entries must name a concrete path). Edits apply immediately."
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/client/index.ts
|
|
12
|
+
const SANDBOX_NS = "sandbox-local";
|
|
13
|
+
const FIELDS = ["access"];
|
|
14
|
+
const NS = "settings.sandbox-local";
|
|
15
|
+
const inject = ["locale"];
|
|
16
|
+
function apply(ctx) {
|
|
17
|
+
const t = ctx.locale.bind(NS);
|
|
18
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
19
|
+
zh,
|
|
20
|
+
en
|
|
21
|
+
}), "sandbox-local: field locale");
|
|
22
|
+
ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
|
|
23
|
+
const hints = scope.schemaFormHints;
|
|
24
|
+
const offs = FIELDS.map((key) => hints.describe(SANDBOX_NS, [key], () => ({
|
|
25
|
+
label: t(key),
|
|
26
|
+
hint: t("accessHint")
|
|
27
|
+
})));
|
|
28
|
+
return () => {
|
|
29
|
+
for (const off of offs) off();
|
|
30
|
+
};
|
|
31
|
+
}, "sandbox-local: field wording"));
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { NS, SANDBOX_NS, apply, inject };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _deepseek_ai_dsh_fs = require("@deepseek-ai/dsh-fs");
|
|
25
|
+
let _deepseek_ai_dsh_fs_local = require("@deepseek-ai/dsh-fs-local");
|
|
26
|
+
let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
|
|
27
|
+
_deepseek_ai_schemastery = __toESM(_deepseek_ai_schemastery, 1);
|
|
28
|
+
let node_fs_promises = require("node:fs/promises");
|
|
29
|
+
let node_path = require("node:path");
|
|
30
|
+
let _deepseek_ai_dsh_sandbox = require("@deepseek-ai/dsh-sandbox");
|
|
31
|
+
let _deepseek_ai_dsh_sandbox_local = require("@deepseek-ai/dsh-sandbox-local");
|
|
32
|
+
//#region src/config.ts
|
|
33
|
+
const localized = (text) => text;
|
|
34
|
+
const Config = _deepseek_ai_schemastery.default.object({
|
|
35
|
+
access: _deepseek_ai_schemastery.default.union([_deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()), _deepseek_ai_schemastery.default.string()]).default([]).description(localized({
|
|
36
|
+
zh: "额外可写根与拒绝项,一行一条:`rw <路径>` 追加可写根,`r- <路径>` 只读,`-- <路径>` 拒绝;路径可用环境变量模板与 glob(`rw` 项必须是具体路径)。改它当场生效(沙箱规则每次按当前值重算),不用重挂这一行。",
|
|
37
|
+
en: "Extra writable roots and refusals, one entry per line: `rw <path>` grants a writable root, `r- <path>` makes it read-only, `-- <path>` denies it; env templates and globs work (`rw` entries must name a concrete path). Edits take effect on the next rule compile without remounting the row."
|
|
38
|
+
})).volatile(),
|
|
39
|
+
runnerCommand: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).default([]).disabled().description(localized({
|
|
40
|
+
zh: "沙箱 runner 的启动命令;空数组表示按平台自带的链去选。改它要重挂这一行。",
|
|
41
|
+
en: "Runner command for confined subprocesses; an empty array picks the platform chain. Edits apply on remount."
|
|
42
|
+
})).volatile(),
|
|
43
|
+
runnerFailureSignatures: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).default([]).disabled().description(localized({
|
|
44
|
+
zh: "runner 命令「用不了」的判定片段,与 `runnerCommand` 成对出现。",
|
|
45
|
+
en: "Fragments that mark the runner command unusable; declared together with `runnerCommand`."
|
|
46
|
+
})).volatile(),
|
|
47
|
+
probeTimeoutMs: _deepseek_ai_schemastery.default.natural().default(5e3).disabled().description(localized({
|
|
48
|
+
zh: "探测 runner 可用性的超时(毫秒)。",
|
|
49
|
+
en: "Timeout in ms for probing whether the runner works."
|
|
50
|
+
})).volatile(),
|
|
51
|
+
cwd: _deepseek_ai_schemastery.default.string().default(process.cwd()).disabled().description(localized({
|
|
52
|
+
zh: "相对路径的基准目录;默认是启动这个进程时的工作目录。",
|
|
53
|
+
en: "Base directory for relative paths; defaults to the process working directory."
|
|
54
|
+
})).volatile(),
|
|
55
|
+
diffBasisMaxBytes: _deepseek_ai_schemastery.default.number().default(10485760).disabled().description(localized({
|
|
56
|
+
zh: "覆盖写差异的单侧字节上限(默认 10 MiB)。",
|
|
57
|
+
en: "Per-side byte cap for overwrite diffs (10 MiB by default)."
|
|
58
|
+
})).volatile()
|
|
59
|
+
});
|
|
60
|
+
function upstreamConfigOf(config) {
|
|
61
|
+
return {
|
|
62
|
+
runnerCommand: [...config.runnerCommand.get()],
|
|
63
|
+
runnerFailureSignatures: [...config.runnerFailureSignatures.get()],
|
|
64
|
+
probeTimeoutMs: config.probeTimeoutMs.get(),
|
|
65
|
+
cwd: config.cwd.get(),
|
|
66
|
+
diffBasisMaxBytes: config.diffBasisMaxBytes.get()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/containment.ts
|
|
71
|
+
const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
72
|
+
async function statIfPresent(path) {
|
|
73
|
+
try {
|
|
74
|
+
return await (0, node_fs_promises.stat)(path, { bigint: true });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const code = error.code;
|
|
77
|
+
if (code !== void 0 && MISSING_CODES.has(code)) return void 0;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function comparablePath(path, caseSensitive) {
|
|
82
|
+
return caseSensitive ? path : path.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
function isLexicallyUnder(path, root, caseSensitive) {
|
|
85
|
+
const comparableTarget = comparablePath(path, caseSensitive);
|
|
86
|
+
const comparableRoot = comparablePath(root, caseSensitive);
|
|
87
|
+
if (comparableTarget === comparableRoot) return true;
|
|
88
|
+
const prefix = comparableRoot.endsWith(node_path.sep) ? comparableRoot : comparableRoot + node_path.sep;
|
|
89
|
+
return comparableTarget.startsWith(prefix);
|
|
90
|
+
}
|
|
91
|
+
function sameIdentity(left, right) {
|
|
92
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
93
|
+
}
|
|
94
|
+
async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
|
|
95
|
+
if (isLexicallyUnder(path, root, caseSensitive)) return true;
|
|
96
|
+
const rootInfo = await statIfPresent(root);
|
|
97
|
+
if (rootInfo === void 0) return false;
|
|
98
|
+
let ancestor = path;
|
|
99
|
+
for (;;) {
|
|
100
|
+
const ancestorInfo = await statIfPresent(ancestor);
|
|
101
|
+
if (ancestorInfo !== void 0 && sameIdentity(ancestorInfo, rootInfo)) return true;
|
|
102
|
+
const parent = (0, node_path.dirname)(ancestor);
|
|
103
|
+
if (parent === ancestor) return false;
|
|
104
|
+
ancestor = parent;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/rules.ts
|
|
109
|
+
const ENV_TEMPLATE = /\{\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
110
|
+
const ACCESS_LINE = /^(rw|r-|--)(?:\s+(.*))?$/u;
|
|
111
|
+
const GLOB_META = /[*?[]/;
|
|
112
|
+
const REGEX_META = /[\\^$+.(){}|]/;
|
|
113
|
+
function expandEnvTemplates(value, env) {
|
|
114
|
+
return value.replace(ENV_TEMPLATE, (_match, name) => {
|
|
115
|
+
const resolved = env[name];
|
|
116
|
+
if (resolved === void 0 || resolved.length === 0) throw new Error(`sandbox rules: "${value}" references the unset environment variable "${name}"`);
|
|
117
|
+
return resolved;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function globToRegexSource(pattern) {
|
|
121
|
+
let source = "";
|
|
122
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
123
|
+
const char = pattern[index];
|
|
124
|
+
if (char === "*") {
|
|
125
|
+
if (pattern[index + 1] === "*") {
|
|
126
|
+
index += 1;
|
|
127
|
+
if (pattern[index + 1] === "/") {
|
|
128
|
+
index += 1;
|
|
129
|
+
source += "(.*/)?";
|
|
130
|
+
} else source += ".*";
|
|
131
|
+
} else source += "[^/]*";
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "?") {
|
|
135
|
+
source += "[^/]";
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (char === "[") {
|
|
139
|
+
const close = pattern.indexOf("]", index + 1);
|
|
140
|
+
if (close === -1) {
|
|
141
|
+
source += "\\[";
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const body = pattern.slice(index + 1, close);
|
|
145
|
+
if (body.length === 0) {
|
|
146
|
+
source += "\\[\\]";
|
|
147
|
+
index = close;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
source += body.startsWith("!") ? `[^${body.slice(1)}]` : `[${body}]`;
|
|
151
|
+
index = close;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
source += REGEX_META.test(char) ? `\\${char}` : char;
|
|
155
|
+
}
|
|
156
|
+
return source;
|
|
157
|
+
}
|
|
158
|
+
function isEmptyRules(rules) {
|
|
159
|
+
return rules.allowRoots.length === 0 && rules.readOnlySubtrees.length === 0 && rules.readOnlyPatterns.length === 0 && rules.denySubtrees.length === 0 && rules.denyPatterns.length === 0;
|
|
160
|
+
}
|
|
161
|
+
function withoutAllowRoots(rules) {
|
|
162
|
+
return rules.allowRoots.length === 0 ? rules : {
|
|
163
|
+
...rules,
|
|
164
|
+
allowRoots: []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseAccess(input) {
|
|
168
|
+
const lines = (input === void 0 ? [] : typeof input === "string" ? [input] : [...input]).flatMap((value) => value.split(/\r?\n/u)).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
169
|
+
const allowWrite = [];
|
|
170
|
+
const readOnly = [];
|
|
171
|
+
const deny = [];
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
const match = ACCESS_LINE.exec(line);
|
|
174
|
+
if (match === null) throw new Error(`sandbox rules: access entry ${JSON.stringify(line)} must start with "rw " (write), "r- " (read-only) or "-- " (deny)`);
|
|
175
|
+
const rule = (match[2] ?? "").trim();
|
|
176
|
+
if (rule.length === 0) throw new Error(`sandbox rules: access entry ${JSON.stringify(line)} carries no path`);
|
|
177
|
+
if (match[1] === "rw") allowWrite.push(rule);
|
|
178
|
+
else if (match[1] === "r-") readOnly.push(rule);
|
|
179
|
+
else deny.push(rule);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
allowWrite,
|
|
183
|
+
readOnly,
|
|
184
|
+
deny
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function ruleSourceOf(access, env) {
|
|
188
|
+
const parsed = parseAccess(access);
|
|
189
|
+
const expand = (values) => values.map((value) => expandEnvTemplates(value, env));
|
|
190
|
+
return {
|
|
191
|
+
allowWrite: expand(parsed.allowWrite),
|
|
192
|
+
readOnly: expand(parsed.readOnly),
|
|
193
|
+
deny: expand(parsed.deny)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function absolutize(value, workspaceRoot) {
|
|
197
|
+
return (0, node_path.isAbsolute)(value) ? value : (0, node_path.resolve)(workspaceRoot, value);
|
|
198
|
+
}
|
|
199
|
+
function compilePaths(values, workspaceRoot) {
|
|
200
|
+
const subtrees = [];
|
|
201
|
+
const patterns = [];
|
|
202
|
+
for (const value of values) {
|
|
203
|
+
const absolute = absolutize(value, workspaceRoot);
|
|
204
|
+
if (GLOB_META.test(value)) {
|
|
205
|
+
const pattern = `^${globToRegexSource(absolute)}$`;
|
|
206
|
+
patterns.push({
|
|
207
|
+
source: pattern,
|
|
208
|
+
regex: new RegExp(pattern)
|
|
209
|
+
});
|
|
210
|
+
} else subtrees.push((0, _deepseek_ai_dsh_sandbox.canonicalPath)(absolute));
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
subtrees,
|
|
214
|
+
patterns
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function compileRules(source, workspaceRoot) {
|
|
218
|
+
const allowRoots = [];
|
|
219
|
+
for (const value of source.allowWrite) {
|
|
220
|
+
if (GLOB_META.test(value)) throw new Error(`sandbox rules: rw entry "${value}" must name a concrete path, not a glob`);
|
|
221
|
+
allowRoots.push((0, _deepseek_ai_dsh_sandbox.canonicalPath)(absolutize(value, workspaceRoot)));
|
|
222
|
+
}
|
|
223
|
+
const readOnly = compilePaths(source.readOnly, workspaceRoot);
|
|
224
|
+
const deny = compilePaths(source.deny, workspaceRoot);
|
|
225
|
+
return {
|
|
226
|
+
allowRoots,
|
|
227
|
+
readOnlySubtrees: readOnly.subtrees,
|
|
228
|
+
readOnlyPatterns: readOnly.patterns,
|
|
229
|
+
denySubtrees: deny.subtrees,
|
|
230
|
+
denyPatterns: deny.patterns
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function writableRootsWith(rules, policy) {
|
|
234
|
+
const roots = (0, _deepseek_ai_dsh_sandbox.writableRoots)(policy);
|
|
235
|
+
return policy.mode === "workspace-write" ? [...roots, ...rules.allowRoots] : roots;
|
|
236
|
+
}
|
|
237
|
+
function isUnder(path, root) {
|
|
238
|
+
if (path === root) return true;
|
|
239
|
+
return path.startsWith(root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`);
|
|
240
|
+
}
|
|
241
|
+
function matches(canonicalTarget, subtrees, patterns) {
|
|
242
|
+
for (const subtree of subtrees) if (isUnder(canonicalTarget, subtree)) return true;
|
|
243
|
+
return patterns.some((pattern) => pattern.regex.test(canonicalTarget));
|
|
244
|
+
}
|
|
245
|
+
function isDenied(rules, canonicalTarget) {
|
|
246
|
+
return matches(canonicalTarget, rules.denySubtrees, rules.denyPatterns);
|
|
247
|
+
}
|
|
248
|
+
function isReadOnly(rules, canonicalTarget) {
|
|
249
|
+
return matches(canonicalTarget, rules.readOnlySubtrees, rules.readOnlyPatterns);
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/fs.ts
|
|
253
|
+
var ConfigurableFileSystem = class extends _deepseek_ai_dsh_fs_local.LocalFileSystem {
|
|
254
|
+
static inject = ["sandboxPolicy"];
|
|
255
|
+
defaultMode;
|
|
256
|
+
access;
|
|
257
|
+
compiled = /* @__PURE__ */ new Map();
|
|
258
|
+
constructor(ctx, config) {
|
|
259
|
+
super(ctx, upstreamConfigOf(config));
|
|
260
|
+
this.defaultMode = ctx.sandboxPolicy.defaultMode;
|
|
261
|
+
this.access = config.access;
|
|
262
|
+
}
|
|
263
|
+
get source() {
|
|
264
|
+
return ruleSourceOf(this.access.get(), process.env);
|
|
265
|
+
}
|
|
266
|
+
get sandboxMode() {
|
|
267
|
+
return this.defaultMode;
|
|
268
|
+
}
|
|
269
|
+
async resolve(path, opts) {
|
|
270
|
+
const target = await super.resolve(path, opts);
|
|
271
|
+
this.assertNotDenied(this.rulesFor(this.ctx.sandboxPolicy.resolve().workspaceRoot), target.targetKey, target.displayPath);
|
|
272
|
+
return target;
|
|
273
|
+
}
|
|
274
|
+
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
275
|
+
return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
|
|
276
|
+
}
|
|
277
|
+
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
278
|
+
return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
|
|
279
|
+
}
|
|
280
|
+
assertNotDenied(rules, canonicalTarget, displayPath) {
|
|
281
|
+
if (!isDenied(rules, canonicalTarget)) return;
|
|
282
|
+
throw new _deepseek_ai_dsh_fs.FsError(`cannot access "${displayPath}": file access denied by the configured "--" rules`, "FS_SANDBOX_DENIED");
|
|
283
|
+
}
|
|
284
|
+
assertWritable(rules, canonicalTarget, displayPath) {
|
|
285
|
+
this.assertNotDenied(rules, canonicalTarget, displayPath);
|
|
286
|
+
if (!isReadOnly(rules, canonicalTarget)) return;
|
|
287
|
+
throw new _deepseek_ai_dsh_fs.FsError(`cannot write "${displayPath}": path is read-only by the configured "r-" rule`, "FS_SANDBOX_DENIED");
|
|
288
|
+
}
|
|
289
|
+
async checkedTarget(target, sandboxPolicy) {
|
|
290
|
+
const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
291
|
+
const rules = this.rulesFor(policy.workspaceRoot);
|
|
292
|
+
this.assertWritable(rules, target.targetKey, target.displayPath);
|
|
293
|
+
const { mode } = policy;
|
|
294
|
+
if (mode === "danger-full-access") return target;
|
|
295
|
+
if (mode === "read-only") throw new _deepseek_ai_dsh_fs.FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
|
|
296
|
+
const fresh = await super.resolve(target.displayPath);
|
|
297
|
+
this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
|
|
298
|
+
for (const root of writableRootsWith(rules, policy)) if (await isPathUnder(fresh.targetKey, root)) return fresh;
|
|
299
|
+
throw new _deepseek_ai_dsh_fs.FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
|
|
300
|
+
}
|
|
301
|
+
rulesFor(workspaceRoot) {
|
|
302
|
+
const access = this.access.get();
|
|
303
|
+
const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
|
|
304
|
+
const cached = this.compiled.get(key);
|
|
305
|
+
if (cached !== void 0) return cached;
|
|
306
|
+
const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
|
|
307
|
+
this.compiled.set(key, compiled);
|
|
308
|
+
return compiled;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/policy.ts
|
|
313
|
+
const SANDBOX_POLICY_CONTEXT = "sandbox:policy";
|
|
314
|
+
const APPROVAL_POLICY_CONTEXT = "approval:policy";
|
|
315
|
+
function renderPolicyContext(policy, rules) {
|
|
316
|
+
const base = (() => {
|
|
317
|
+
switch (policy.mode) {
|
|
318
|
+
case "read-only": return "文件策略:只读——不能修改任何文件。别因此拒绝必需的修改:照常尝试工具,按它的拒绝与升级指引行动。";
|
|
319
|
+
case "workspace-write": return `文件策略:workspace-write——可改会话工作区 ${JSON.stringify(policy.workspaceRoot)} 下的文件,部分平台临时目录同样可写。`;
|
|
320
|
+
case "danger-full-access": return "文件策略:全权——文件改动不再受限。";
|
|
321
|
+
}
|
|
322
|
+
})();
|
|
323
|
+
const extras = [];
|
|
324
|
+
if (rules.allowWrite.length > 0) extras.push(`额外可写:${rules.allowWrite.join("、")}。`);
|
|
325
|
+
if (rules.readOnly.length > 0) extras.push(`只读(不可写):${rules.readOnly.join("、")}。`);
|
|
326
|
+
if (rules.deny.length > 0) extras.push(`拒绝(读写都拒):${rules.deny.join("、")}。`);
|
|
327
|
+
return extras.length === 0 ? base : `${base} ${extras.join("")}`;
|
|
328
|
+
}
|
|
329
|
+
const APPROVAL_ASK_TEXT = "审批策略:ask——需要审批的操作会询问用户;没有可用的应答者时直接失败。";
|
|
330
|
+
const APPROVAL_NEVER_TEXT = "审批提示已禁用:需要审批的操作一律自动拒绝——不要请求沙箱升级(不要设 sandbox_permissions)。";
|
|
331
|
+
function renderApprovalContext(policy) {
|
|
332
|
+
return policy === "never" ? APPROVAL_NEVER_TEXT : APPROVAL_ASK_TEXT;
|
|
333
|
+
}
|
|
334
|
+
function effectiveApprovalPolicy(approval, session) {
|
|
335
|
+
return approval.overrideOf(session) ?? approval.config.policy ?? "ask";
|
|
336
|
+
}
|
|
337
|
+
function registerPolicyContext(scope, rules, resolve) {
|
|
338
|
+
const prompt = scope.get("systemPrompt");
|
|
339
|
+
if (prompt === void 0) return;
|
|
340
|
+
prompt.context({
|
|
341
|
+
name: SANDBOX_POLICY_CONTEXT,
|
|
342
|
+
order: prompt.getContextOrder("SANDBOX_POLICY"),
|
|
343
|
+
text: (context) => {
|
|
344
|
+
const session = context.agent?.session;
|
|
345
|
+
return session === void 0 ? "" : renderPolicyContext(resolve(session), rules);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function registerApprovalContext(scope) {
|
|
350
|
+
const prompt = scope.get("systemPrompt");
|
|
351
|
+
if (prompt === void 0) return;
|
|
352
|
+
prompt.context({
|
|
353
|
+
name: APPROVAL_POLICY_CONTEXT,
|
|
354
|
+
order: prompt.getContextOrder("APPROVAL_POLICY"),
|
|
355
|
+
text: (context) => {
|
|
356
|
+
const agent = context.agent;
|
|
357
|
+
if (agent === void 0) return "";
|
|
358
|
+
const approval = agent.ctx.get("approval");
|
|
359
|
+
return approval === void 0 ? "" : renderApprovalContext(effectiveApprovalPolicy(approval, agent.session));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function installRuntimeContexts(ctx, rules, resolve) {
|
|
364
|
+
const installed = /* @__PURE__ */ new WeakSet();
|
|
365
|
+
const install = (agent) => {
|
|
366
|
+
if (installed.has(agent)) return;
|
|
367
|
+
installed.add(agent);
|
|
368
|
+
registerPolicyContext(agent.ctx, rules, resolve);
|
|
369
|
+
registerApprovalContext(agent.ctx);
|
|
370
|
+
};
|
|
371
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
372
|
+
install(agent);
|
|
373
|
+
});
|
|
374
|
+
for (const agent of ctx.get("agents")?.list() ?? []) install(agent);
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/dialects.ts
|
|
378
|
+
const SEPARATOR = "--";
|
|
379
|
+
function separatorIndex(argv) {
|
|
380
|
+
const index = argv.indexOf(SEPARATOR);
|
|
381
|
+
if (index === -1) throw new Error("sandbox rules: the confined argv carries no `--` separator to extend");
|
|
382
|
+
return index;
|
|
383
|
+
}
|
|
384
|
+
function dialectOf(argv) {
|
|
385
|
+
const separator = argv.indexOf(SEPARATOR);
|
|
386
|
+
const runner = separator === -1 ? argv : argv.slice(0, separator);
|
|
387
|
+
if (runner.includes("-p")) return "seatbelt";
|
|
388
|
+
if (runner[0] === "bwrap") return "bwrap";
|
|
389
|
+
if (runner.includes("--workspace")) return "windows-acl";
|
|
390
|
+
if (runner.includes("--ro") || runner.includes("--rw")) return "landlock";
|
|
391
|
+
}
|
|
392
|
+
function sbplString(value) {
|
|
393
|
+
return `"${value.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`)}"`;
|
|
394
|
+
}
|
|
395
|
+
function sbplRegexBody(source) {
|
|
396
|
+
if (source.includes("\"")) throw new Error(`sandbox rules: deny pattern ${JSON.stringify(source)} cannot be expressed in a Seatbelt profile`);
|
|
397
|
+
return source;
|
|
398
|
+
}
|
|
399
|
+
function extendSeatbelt(argv, rules) {
|
|
400
|
+
const index = argv.indexOf("-p");
|
|
401
|
+
const profile = index === -1 ? void 0 : argv[index + 1];
|
|
402
|
+
if (profile === void 0) throw new Error("sandbox rules: the Seatbelt runner argv carries no `-p` profile to extend");
|
|
403
|
+
const extended = `${profile} ${[
|
|
404
|
+
...rules.allowRoots.map((root) => `(allow file-write* (subpath ${sbplString(root)}))`),
|
|
405
|
+
...rules.readOnlySubtrees.map((path) => `(deny file-write* (subpath ${sbplString(path)}))`),
|
|
406
|
+
...rules.readOnlyPatterns.map((pattern) => `(deny file-write* (regex #"${sbplRegexBody(pattern.source)}"))`),
|
|
407
|
+
...rules.denySubtrees.map((path) => `(deny file-read* file-write* (subpath ${sbplString(path)}))`),
|
|
408
|
+
...rules.denyPatterns.map((pattern) => `(deny file-read* file-write* (regex #"${sbplRegexBody(pattern.source)}"))`)
|
|
409
|
+
].join(" ")}`;
|
|
410
|
+
return [
|
|
411
|
+
...argv.slice(0, index + 1),
|
|
412
|
+
extended,
|
|
413
|
+
...argv.slice(index + 2)
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
function extendBwrap(argv, rules) {
|
|
417
|
+
const separator = separatorIndex(argv);
|
|
418
|
+
const additions = [];
|
|
419
|
+
for (const root of rules.allowRoots) additions.push("--bind-try", root, root);
|
|
420
|
+
for (const path of [...rules.readOnlySubtrees, ...rules.denySubtrees]) additions.push("--ro-bind-try", path, path);
|
|
421
|
+
return [
|
|
422
|
+
...argv.slice(0, separator),
|
|
423
|
+
...additions,
|
|
424
|
+
...argv.slice(separator)
|
|
425
|
+
];
|
|
426
|
+
}
|
|
427
|
+
function extendLandlock(argv, rules) {
|
|
428
|
+
const separator = separatorIndex(argv);
|
|
429
|
+
const additions = rules.allowRoots.flatMap((root) => ["--rw", root]);
|
|
430
|
+
return [
|
|
431
|
+
...argv.slice(0, separator),
|
|
432
|
+
...additions,
|
|
433
|
+
...argv.slice(separator)
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
function extendConfinedArgv(argv, rules) {
|
|
437
|
+
if (isEmptyRules(rules)) return [...argv];
|
|
438
|
+
const dialect = dialectOf(argv);
|
|
439
|
+
if (dialect === void 0) throw new Error("sandbox rules: cannot extend an unrecognized sandbox runner argv; drop allowWrite/deny or configure a bwrap-compatible runnerCommand");
|
|
440
|
+
switch (dialect) {
|
|
441
|
+
case "seatbelt": return extendSeatbelt(argv, rules);
|
|
442
|
+
case "bwrap": return extendBwrap(argv, rules);
|
|
443
|
+
case "landlock": return extendLandlock(argv, rules);
|
|
444
|
+
case "windows-acl": return [...argv];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/sandbox.ts
|
|
449
|
+
var ConfigurableSandboxProvider = class extends _deepseek_ai_dsh_sandbox_local.LocalSandboxProvider {
|
|
450
|
+
access;
|
|
451
|
+
compiled = /* @__PURE__ */ new Map();
|
|
452
|
+
constructor(ctx, config) {
|
|
453
|
+
super(ctx, upstreamConfigOf(config));
|
|
454
|
+
this.access = config.access;
|
|
455
|
+
}
|
|
456
|
+
get source() {
|
|
457
|
+
return ruleSourceOf(this.access.get(), process.env);
|
|
458
|
+
}
|
|
459
|
+
async confine(argv, policy, signal) {
|
|
460
|
+
const confined = await super.confine(argv, policy, signal);
|
|
461
|
+
const rules = this.rulesFor(policy.workspaceRoot);
|
|
462
|
+
const effective = policy.mode === "workspace-write" ? rules : withoutAllowRoots(rules);
|
|
463
|
+
if (isEmptyRules(effective)) return confined;
|
|
464
|
+
return {
|
|
465
|
+
...confined,
|
|
466
|
+
argv: extendConfinedArgv(confined.argv, effective)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
rulesFor(workspaceRoot) {
|
|
470
|
+
const access = this.access.get();
|
|
471
|
+
const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
|
|
472
|
+
const cached = this.compiled.get(key);
|
|
473
|
+
if (cached !== void 0) return cached;
|
|
474
|
+
const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
|
|
475
|
+
this.compiled.set(key, compiled);
|
|
476
|
+
return compiled;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
//#endregion
|
|
480
|
+
//#region src/index.ts
|
|
481
|
+
const name = "sandbox-local";
|
|
482
|
+
const inject = ["sandboxPolicy"];
|
|
483
|
+
function warnAboutDegradedRules(ctx, rules) {
|
|
484
|
+
const grants = rules.allowWrite.length > 0;
|
|
485
|
+
const readOnly = rules.readOnly.length > 0;
|
|
486
|
+
const denials = rules.deny.length > 0;
|
|
487
|
+
if (!grants && !readOnly && !denials) return;
|
|
488
|
+
if (process.platform === "darwin") return;
|
|
489
|
+
if (readOnly || denials) ctx.logger.warn(`sandbox-local: "r-" / "--" entries cannot be fully enforced for confined subprocesses on ${process.platform} (Seatbelt enforces both; bwrap binds the path read-only, so "--" degrades to write-only; Landlock and the Windows ACL runner cannot express a subpath rule at all) — tools that read through ctx.fs stay covered`);
|
|
490
|
+
if (grants && process.platform === "win32") ctx.logger.warn("sandbox-local: \"rw\" entries cannot be granted to confined subprocesses on win32; ctx.fs covers the extra roots, the Windows ACL runner does not");
|
|
491
|
+
}
|
|
492
|
+
function apply(ctx, config) {
|
|
493
|
+
const rules = ruleSourceOf(config.access.get(), process.env);
|
|
494
|
+
warnAboutDegradedRules(ctx, rules);
|
|
495
|
+
new ConfigurableSandboxProvider(ctx, config);
|
|
496
|
+
new ConfigurableFileSystem(ctx, config);
|
|
497
|
+
installRuntimeContexts(ctx, rules, (session) => ctx.sandboxPolicy.resolve({ session }));
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
exports.Config = Config;
|
|
501
|
+
exports.apply = apply;
|
|
502
|
+
exports.inject = inject;
|
|
503
|
+
exports.name = name;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Config as Config$1 } from "@deepseek-ai/dsh-fs-local";
|
|
2
|
-
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { Config as Config$2 } from "@deepseek-ai/dsh-sandbox-local";
|
|
4
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
2
|
import { Volatile } from "@deepseek-ai/cosmokit";
|
|
3
|
+
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import { Config as Config$1 } from "@deepseek-ai/dsh-fs-local";
|
|
5
|
+
import { Config as Config$2 } from "@deepseek-ai/dsh-sandbox-local";
|
|
6
6
|
//#region src/config.d.ts
|
|
7
7
|
export interface Config extends Config$2, Config$1 {
|
|
8
8
|
access?: string | string[];
|
package/dist/rows.cjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/rows.ts
|
|
3
|
+
function sandboxRows() {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
id: "sandbox",
|
|
7
|
+
disabled: true
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "fs-sandbox",
|
|
11
|
+
disabled: true
|
|
12
|
+
},
|
|
13
|
+
{ insert: [{
|
|
14
|
+
id: "sandbox-local",
|
|
15
|
+
name: "@morlay/dsh-sandbox-local"
|
|
16
|
+
}] }
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
exports.sandboxRows = sandboxRows;
|