@morlay/dsh-sandbox-local 0.0.2 → 0.0.3-alpha.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/README.md +13 -38
- package/dist/index.d.mts +0 -13
- package/dist/index.mjs +2 -189
- package/package.json +14 -14
- package/src/config.ts +0 -15
- package/src/containment.ts +0 -23
- package/src/dialects.ts +3 -49
- package/src/fs.ts +2 -53
- package/src/index.ts +0 -29
- package/src/rules.ts +7 -89
- package/src/sandbox.ts +7 -25
package/README.md
CHANGED
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
上游沙箱策略只有两个字段:`mode`(`read-only` / `workspace-write` /
|
|
10
10
|
`danger-full-access`)与 `workspaceRoot`;`workspace-write` 的可写路径是硬编码的
|
|
11
|
-
`[工作区, /tmp, os.tmpdir()]`(`packages/sandbox/sandbox/src/roots.ts:52-55`),
|
|
11
|
+
`[工作区, /tmp, os.tmpdir()]`(`vendor/deepseek-harness/packages/sandbox/sandbox/src/roots.ts:52-55`),
|
|
12
12
|
没有任何追加可写根或拒绝项的配置面。于是「让 agent 能写 `$XDG_CACHE_HOME`,
|
|
13
|
-
但永远不许碰项目里的 `mise.*.toml
|
|
14
|
-
(`readDenyPaths`)又撤回:bwrap 要在已置只读的树里创建挂载点、Landlock 无法从自己的
|
|
15
|
-
`/` 读授权里减除,一个「在能生效的地方破坏隔离、在不能生效的地方谎报」的保护被判为
|
|
16
|
-
不如明确的缺失(`.agents/notes/implemented/architecture/2026-07-30-credential-boundaries-and-atomic-registration.md:29`)。
|
|
13
|
+
但永远不许碰项目里的 `mise.*.toml`」这类诉求只能整块放弃隔离。
|
|
17
14
|
|
|
18
15
|
本包把「能表达多少就说多少」明确下来:Seatbelt 完整生效,其余平台按方言降级,
|
|
19
16
|
并在加载期告警,而不是静默失效。
|
|
@@ -92,39 +89,17 @@
|
|
|
92
89
|
|
|
93
90
|
## 装配
|
|
94
91
|
|
|
95
|
-
本包自带 `cordis.patch.yml
|
|
96
|
-
|
|
92
|
+
本包自带 `cordis.patch.yml`(禁用官方 `sandbox` / `fs-sandbox` 两行 + 插入自己的一行),
|
|
93
|
+
把本包作为**独立 bundle** 采用的部署直接列进 `dsh.profile.bundles` 即可;行不带 config
|
|
94
|
+
(schema 默认是空规则),patch 内容见该文件。
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
disabled: true
|
|
104
|
-
|
|
105
|
-
- insert:
|
|
106
|
-
- id: sandbox-local
|
|
107
|
-
name: "@morlay/dsh-sandbox-local"
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**本部署(`@morlay/dsh-preset`)不走这条路径**:它的 patch 自己禁用官方两行、插入
|
|
111
|
-
`- id: sandbox-local` 行并写上规则,因此示例 app 的 `dsh.profile.bundles` 不需要列出本包,
|
|
112
|
-
只需要 profile 的依赖树能解析模块名(`@morlay/dsh-preset` 已在 `dependencies` 声明本包)。
|
|
96
|
+
**本部署(`@morlay/dsh-preset`)不走这条路径**:装配(禁用官方两行 + 插入
|
|
97
|
+
`sandbox-local` 行)与 `access` 规则一起维护在 preset 的 bundle patch 里,因此示例 app 的
|
|
98
|
+
`dsh.profile.bundles` 不需要列出本包,只需要 profile 的依赖树能解析模块名
|
|
99
|
+
(`@morlay/dsh-preset` 已在 `dependencies` 声明本包)。patch 层级的合并顺序与放置理由见
|
|
100
|
+
[设计 预设生成与装配](../../preset/dsh-preset/.agents/designs/20260917-预设生成与装配.md)。
|
|
113
101
|
两种采用方式互斥:同时上线会重复插入同一行。
|
|
114
102
|
|
|
115
|
-
**规则写在哪里才会生效**(patch 层按 `[bundle patches, profile patches, home patches,
|
|
116
|
-
overlays]` 合并,后应用者整块替换同一行的 `config`):
|
|
117
|
-
|
|
118
|
-
| 载体 | 生效范围 |
|
|
119
|
-
| ----------------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
120
|
-
| profile 的 `cordis.patch.yml`(`$DSH_HOME/profiles/<name>/cordis.patch.yml`) | dev 与打包形态都生效,但只属于本机 home |
|
|
121
|
-
| app 的 `cordis.patch.yml` | 打包(`bundle`)形态:作为 seed 的 profile patch 生效 |
|
|
122
|
-
| 一个自有 bundle 的 patch | 所有形态(随包分发) |
|
|
123
|
-
|
|
124
|
-
本部署采用最后一种:**装配与规则在 `@morlay/dsh-preset` 的 bundle patch 里一起维护**
|
|
125
|
-
(禁用官方两行 + 插入本包行 + `access` 规则,跨 dev / 打包形态一致),不依赖 app 的
|
|
126
|
-
`dsh.profile.bundles` 再列一层。
|
|
127
|
-
|
|
128
103
|
## 前提
|
|
129
104
|
|
|
130
105
|
- 官方 `sandbox` 与 `fs-sandbox` 行必须禁用:同一 scope 内重复注册同名服务会 fail loud
|
|
@@ -154,7 +129,7 @@ overlays]` 合并,后应用者整块替换同一行的 `config`):
|
|
|
154
129
|
- **Windows 额外授权未实现**:官方 `AclWriteGrant` 可以做预授权,但没有把 `AclWriteGrant`
|
|
155
130
|
接进 `confine` 的现成路径,本版只告警。
|
|
156
131
|
|
|
157
|
-
##
|
|
132
|
+
## 验证
|
|
158
133
|
|
|
159
|
-
|
|
160
|
-
|
|
134
|
+
构建、测试与 lint 走根 `justfile`(含 `seatbelt.e2e.spec.ts`——非 macOS 或被更外层 Seatbelt
|
|
135
|
+
拦住时自动跳过);本包的接缝与判据见 [`.agents/standards/`](./.agents/standards/)。
|
package/dist/index.d.mts
CHANGED
|
@@ -3,27 +3,14 @@ import { Config as Config$2 } from "@deepseek-ai/dsh-sandbox-local";
|
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
5
|
//#region src/config.d.ts
|
|
6
|
-
/** 插件配置。 */
|
|
7
6
|
interface Config extends Config$2, Config$1 {
|
|
8
|
-
/**
|
|
9
|
-
* 规则条目:`rw <path>`(额外可写根)、`r- <path>`(只读)、`-- <pattern>`(拒绝访问)。
|
|
10
|
-
* 数组每项一条,或写一段多行文本(每行一条);`{{ env.NAME }}` 按进程环境展开。
|
|
11
|
-
*/
|
|
12
7
|
access?: string | string[];
|
|
13
8
|
}
|
|
14
|
-
/** 运行时配置 schema。 */
|
|
15
9
|
declare const Config: z<Config>;
|
|
16
10
|
//#endregion
|
|
17
11
|
//#region src/index.d.ts
|
|
18
|
-
/** Cordis 插件名。 */
|
|
19
12
|
declare const name = "sandbox-local";
|
|
20
|
-
/** fs 侧从策略服务取默认模式与工作区回退根,所以先等 `ctx.sandboxPolicy`。 */
|
|
21
13
|
declare const inject: string[];
|
|
22
|
-
/**
|
|
23
|
-
* 注册两个替换实现。
|
|
24
|
-
* @param ctx - 插件上下文(官方 `sandbox` / `fs-sandbox` 行已禁用)。
|
|
25
|
-
* @param config - 已由 schema 填好默认值的配置。
|
|
26
|
-
*/
|
|
27
14
|
declare function apply(ctx: Context, config: Config): void;
|
|
28
15
|
//#endregion
|
|
29
16
|
export { Config, apply, inject, name };
|
package/dist/index.mjs
CHANGED
|
@@ -6,26 +6,10 @@ import { stat } from "node:fs/promises";
|
|
|
6
6
|
import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
8
8
|
//#region src/rules.ts
|
|
9
|
-
/**
|
|
10
|
-
* 规则编译:`access` 条目解析(`rw <path>` 额外可写根 / `r- <path>` 只读 /
|
|
11
|
-
* `-- <pattern>` 拒绝访问)、`{{ env.NAME }}` 模板展开、相对工作区路径的绝对化、
|
|
12
|
-
* glob → 正则,以及编译结果的匹配。
|
|
13
|
-
*
|
|
14
|
-
* 命中优先级:`--` 拒绝覆盖一切;`r-` 只读覆盖可写授予;都未命中时才按可写根判定。
|
|
15
|
-
*
|
|
16
|
-
* 生成的正则源码同时交给进程内检查(JS `RegExp`)与 macOS Seatbelt profile
|
|
17
|
-
* (SBPL 的 `(regex #"…")`),因此只用 POSIX ERE 与 JS 正则共有的语法。
|
|
18
|
-
* @module @morlay/dsh-sandbox-local/rules
|
|
19
|
-
*/
|
|
20
|
-
/** `{{ env.NAME }}` 模板;名字限定为环境变量的字符集。 */
|
|
21
9
|
const ENV_TEMPLATE = /\{\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
22
|
-
/** `access` 的条目形式:`rw <path>` / `r- <path>` / `-- <pattern>`;前缀后必须有空白。 */
|
|
23
10
|
const ACCESS_LINE = /^(rw|r-|--)(?:\s+(.*))?$/u;
|
|
24
|
-
/** glob 元字符:规则里出现即按模式匹配,否则按字面路径(含其全部后代)匹配。 */
|
|
25
11
|
const GLOB_META = /[*?[]/;
|
|
26
|
-
/** glob 翻译时需要转义的正则元字符(字符类内部除外)。 */
|
|
27
12
|
const REGEX_META = /[\\^$+.(){}|]/;
|
|
28
|
-
/** 展开规则里的 `{{ env.NAME }}`;引用未定义或为空的环境变量直接抛错。 */
|
|
29
13
|
function expandEnvTemplates(value, env) {
|
|
30
14
|
return value.replace(ENV_TEMPLATE, (_match, name) => {
|
|
31
15
|
const resolved = env[name];
|
|
@@ -33,13 +17,6 @@ function expandEnvTemplates(value, env) {
|
|
|
33
17
|
return resolved;
|
|
34
18
|
});
|
|
35
19
|
}
|
|
36
|
-
/**
|
|
37
|
-
* 把含 glob 的规则翻译成正则源码。
|
|
38
|
-
* `**` 跨目录层级(`**` 与“`**` 后接斜杠”都能匹配零层),`*` 与 `?` 不跨 `/`,
|
|
39
|
-
* 字符类透传(`[!…]` 按 glob 习惯翻成 `[^…]`),其余正则元字符转义。
|
|
40
|
-
* @param pattern - 已绝对化的 glob 规则。
|
|
41
|
-
* @returns 可同时被 JS `RegExp` 与 SBPL regex 接受的正则源码。
|
|
42
|
-
*/
|
|
43
20
|
function globToRegexSource(pattern) {
|
|
44
21
|
let source = "";
|
|
45
22
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
@@ -78,24 +55,15 @@ function globToRegexSource(pattern) {
|
|
|
78
55
|
}
|
|
79
56
|
return source;
|
|
80
57
|
}
|
|
81
|
-
/** 规则是否为空——空规则下 provider 不改写任何 runner 参数。 */
|
|
82
58
|
function isEmptyRules(rules) {
|
|
83
59
|
return rules.allowRoots.length === 0 && rules.readOnlySubtrees.length === 0 && rules.readOnlyPatterns.length === 0 && rules.denySubtrees.length === 0 && rules.denyPatterns.length === 0;
|
|
84
60
|
}
|
|
85
|
-
/** 只读模式不因 `rw` 条目放松:投影出只保留 `r-` / `--` 条目的规则集。 */
|
|
86
61
|
function withoutAllowRoots(rules) {
|
|
87
62
|
return rules.allowRoots.length === 0 ? rules : {
|
|
88
63
|
...rules,
|
|
89
64
|
allowRoots: []
|
|
90
65
|
};
|
|
91
66
|
}
|
|
92
|
-
/**
|
|
93
|
-
* 解析 `access` 配置:接受字符串数组(每项一条规则)或多行文本(每行一条规则),
|
|
94
|
-
* 空行忽略;`rw <path>` 是额外可写根,`r- <path>` 是只读,`-- <pattern>` 是访问
|
|
95
|
-
* 拒绝(读 + 写)。缺前缀或前缀后没有路径都直接报错——规则不因写法歧义而变形。
|
|
96
|
-
* @param input - 配置里的 `access` 值。
|
|
97
|
-
* @returns `allowWrite` / `readOnly` / `deny` 三组规则(模板尚未展开)。
|
|
98
|
-
*/
|
|
99
67
|
function parseAccess(input) {
|
|
100
68
|
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);
|
|
101
69
|
const allowWrite = [];
|
|
@@ -116,13 +84,6 @@ function parseAccess(input) {
|
|
|
116
84
|
deny
|
|
117
85
|
};
|
|
118
86
|
}
|
|
119
|
-
/**
|
|
120
|
-
* 把配置里的 `access` 转成规则来源:模板在这一步展开(加载期,fail-fast),
|
|
121
|
-
* 相对路径留待按调用时的工作区根绝对化。
|
|
122
|
-
* @param config - 含 `access` 的插件配置。
|
|
123
|
-
* @param env - 模板展开用的进程环境。
|
|
124
|
-
* @returns 模板已展开的规则来源。
|
|
125
|
-
*/
|
|
126
87
|
function ruleSourceOf(config, env) {
|
|
127
88
|
const parsed = parseAccess(config.access);
|
|
128
89
|
const expand = (values) => values.map((value) => expandEnvTemplates(value, env));
|
|
@@ -132,11 +93,9 @@ function ruleSourceOf(config, env) {
|
|
|
132
93
|
deny: expand(parsed.deny)
|
|
133
94
|
};
|
|
134
95
|
}
|
|
135
|
-
/** 绝对化一条规则:相对路径相对工作区根。 */
|
|
136
96
|
function absolutize(value, workspaceRoot) {
|
|
137
97
|
return isAbsolute(value) ? value : resolve(workspaceRoot, value);
|
|
138
98
|
}
|
|
139
|
-
/** 编译一组字面路径 / glob 规则:字面项按子树(含全部后代)匹配,glob 项按整串匹配。 */
|
|
140
99
|
function compilePaths(values, workspaceRoot) {
|
|
141
100
|
const subtrees = [];
|
|
142
101
|
const patterns = [];
|
|
@@ -155,13 +114,6 @@ function compilePaths(values, workspaceRoot) {
|
|
|
155
114
|
patterns
|
|
156
115
|
};
|
|
157
116
|
}
|
|
158
|
-
/**
|
|
159
|
-
* 按一个工作区根编译规则。
|
|
160
|
-
* `rw` 条目必须是具体路径(glob 无法表达“可写根”);`r-` 与 `--` 条目允许 glob。
|
|
161
|
-
* @param source - 已展开模板的规则来源。
|
|
162
|
-
* @param workspaceRoot - 相对规则解析用的工作区根(canonical 绝对路径)。
|
|
163
|
-
* @returns 编译后的规则。
|
|
164
|
-
*/
|
|
165
117
|
function compileRules(source, workspaceRoot) {
|
|
166
118
|
const allowRoots = [];
|
|
167
119
|
for (const value of source.allowWrite) {
|
|
@@ -178,50 +130,23 @@ function compileRules(source, workspaceRoot) {
|
|
|
178
130
|
denyPatterns: deny.patterns
|
|
179
131
|
};
|
|
180
132
|
}
|
|
181
|
-
/** 字面规则的后代判定(canonical 拼写,与 Seatbelt `subpath` 同一语义)。 */
|
|
182
133
|
function isUnder(path, root) {
|
|
183
134
|
if (path === root) return true;
|
|
184
135
|
return path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
|
|
185
136
|
}
|
|
186
|
-
/** 命中任一子树或任一模式。 */
|
|
187
137
|
function matches(canonicalTarget, subtrees, patterns) {
|
|
188
138
|
for (const subtree of subtrees) if (isUnder(canonicalTarget, subtree)) return true;
|
|
189
139
|
return patterns.some((pattern) => pattern.regex.test(canonicalTarget));
|
|
190
140
|
}
|
|
191
|
-
/**
|
|
192
|
-
* 判断目标是否被 `--` 条目命中(读与写都拒)。
|
|
193
|
-
* @param rules - 已编译规则。
|
|
194
|
-
* @param canonicalTarget - 目标的 canonical 路径。
|
|
195
|
-
* @returns 命中即 true。
|
|
196
|
-
*/
|
|
197
141
|
function isDenied(rules, canonicalTarget) {
|
|
198
142
|
return matches(canonicalTarget, rules.denySubtrees, rules.denyPatterns);
|
|
199
143
|
}
|
|
200
|
-
/**
|
|
201
|
-
* 判断目标是否被 `r-` 条目命中(读放行、写拒绝)。
|
|
202
|
-
* @param rules - 已编译规则。
|
|
203
|
-
* @param canonicalTarget - 目标的 canonical 路径。
|
|
204
|
-
* @returns 命中即 true。
|
|
205
|
-
*/
|
|
206
144
|
function isReadOnly(rules, canonicalTarget) {
|
|
207
145
|
return matches(canonicalTarget, rules.readOnlySubtrees, rules.readOnlyPatterns);
|
|
208
146
|
}
|
|
209
147
|
//#endregion
|
|
210
148
|
//#region src/dialects.ts
|
|
211
|
-
/**
|
|
212
|
-
* 在官方 provider 生成的 confined argv 上追加本包规则:
|
|
213
|
-
* Seatbelt 追加 SBPL 规则(后置规则覆盖先前的 allow,实测 `(deny file-read* file-write*
|
|
214
|
-
* (subpath …))` 能压过 `(allow file-write* (subpath …))`,`r-` 条目则只 deny 写入),
|
|
215
|
-
* bwrap 追加挂载参数(`rw` 用 `--bind-try`,`r-` 与 `--` 都用 `--ro-bind-try`),
|
|
216
|
-
* Landlock 只能追加可写授权(其 allow-list 语义无法减除子路径),
|
|
217
|
-
* Windows ACL runner 没有对应表达。
|
|
218
|
-
*
|
|
219
|
-
* 方言从官方 `ConfinedArgv.argv` 的结构识别:runner 参数在前,`--` 之后是调用方 argv。
|
|
220
|
-
* @module @morlay/dsh-sandbox-local/dialects
|
|
221
|
-
*/
|
|
222
|
-
/** runner 部分与调用方 argv 的分隔符。 */
|
|
223
149
|
const SEPARATOR = "--";
|
|
224
|
-
/** 各方言的规则表达能力——加载期据此告警,运行期据此决定是否改写参数。 */
|
|
225
150
|
const DIALECT_CAPABILITIES = {
|
|
226
151
|
seatbelt: {
|
|
227
152
|
allowWrite: true,
|
|
@@ -248,17 +173,11 @@ const DIALECT_CAPABILITIES = {
|
|
|
248
173
|
denyWriteOnly: false
|
|
249
174
|
}
|
|
250
175
|
};
|
|
251
|
-
/** runner 部分的结束位置。 */
|
|
252
176
|
function separatorIndex(argv) {
|
|
253
177
|
const index = argv.indexOf(SEPARATOR);
|
|
254
178
|
if (index === -1) throw new Error("sandbox rules: the confined argv carries no `--` separator to extend");
|
|
255
179
|
return index;
|
|
256
180
|
}
|
|
257
|
-
/**
|
|
258
|
-
* 识别 argv 使用的执行方言。
|
|
259
|
-
* @param argv - 官方 provider 返回的完整 confined argv。
|
|
260
|
-
* @returns 方言,或无法识别时的 `undefined`(例如运维自定义的 runnerCommand)。
|
|
261
|
-
*/
|
|
262
181
|
function dialectOf(argv) {
|
|
263
182
|
const separator = argv.indexOf(SEPARATOR);
|
|
264
183
|
const runner = separator === -1 ? argv : argv.slice(0, separator);
|
|
@@ -267,19 +186,13 @@ function dialectOf(argv) {
|
|
|
267
186
|
if (runner.includes("--workspace")) return "windows-acl";
|
|
268
187
|
if (runner.includes("--ro") || runner.includes("--rw")) return "landlock";
|
|
269
188
|
}
|
|
270
|
-
/** SBPL 字符串字面量。 */
|
|
271
189
|
function sbplString(value) {
|
|
272
190
|
return `"${value.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`)}"`;
|
|
273
191
|
}
|
|
274
|
-
/**
|
|
275
|
-
* SBPL `#"…"` 的正则体:正则自身的 `\` 必须保留(glob 翻译用它转义元字符),
|
|
276
|
-
* 因此拒绝项里出现双引号时直接报错,而不是产出一个含义变化的 profile。
|
|
277
|
-
*/
|
|
278
192
|
function sbplRegexBody(source) {
|
|
279
193
|
if (source.includes("\"")) throw new Error(`sandbox rules: deny pattern ${JSON.stringify(source)} cannot be expressed in a Seatbelt profile`);
|
|
280
194
|
return source;
|
|
281
195
|
}
|
|
282
|
-
/** Seatbelt:把 allow / deny 规则追加到 `-p` 的 profile 文本末尾。 */
|
|
283
196
|
function extendSeatbelt(argv, rules) {
|
|
284
197
|
const index = argv.indexOf("-p");
|
|
285
198
|
const profile = index === -1 ? void 0 : argv[index + 1];
|
|
@@ -297,11 +210,6 @@ function extendSeatbelt(argv, rules) {
|
|
|
297
210
|
...argv.slice(index + 2)
|
|
298
211
|
];
|
|
299
212
|
}
|
|
300
|
-
/**
|
|
301
|
-
* bwrap:额外可写根用 `--bind-try`(路径不存在时跳过);`r-` 与 `--` 都用
|
|
302
|
-
* `--ro-bind-try` 覆盖成只读(挂载后行覆盖前行)——也就是说 `--` 在 bwrap 上退化为
|
|
303
|
-
* “只拒写入”。glob 模式无法表达为静态挂载,由调用方在加载期告警。
|
|
304
|
-
*/
|
|
305
213
|
function extendBwrap(argv, rules) {
|
|
306
214
|
const separator = separatorIndex(argv);
|
|
307
215
|
const additions = [];
|
|
@@ -313,7 +221,6 @@ function extendBwrap(argv, rules) {
|
|
|
313
221
|
...argv.slice(separator)
|
|
314
222
|
];
|
|
315
223
|
}
|
|
316
|
-
/** Landlock:只能追加可写授权(`--rw`),`r-` 与 `--` 都无对应表达。 */
|
|
317
224
|
function extendLandlock(argv, rules) {
|
|
318
225
|
const separator = separatorIndex(argv);
|
|
319
226
|
const additions = rules.allowRoots.flatMap((root) => ["--rw", root]);
|
|
@@ -323,12 +230,6 @@ function extendLandlock(argv, rules) {
|
|
|
323
230
|
...argv.slice(separator)
|
|
324
231
|
];
|
|
325
232
|
}
|
|
326
|
-
/**
|
|
327
|
-
* 按 argv 的方言追加规则。
|
|
328
|
-
* @param argv - 官方 provider 返回的 confined argv。
|
|
329
|
-
* @param rules - 已编译规则;空规则原样返回。
|
|
330
|
-
* @returns 追加规则后的 argv;方言无法识别时抛错(规则不能静默失效)。
|
|
331
|
-
*/
|
|
332
233
|
function extendConfinedArgv(argv, rules) {
|
|
333
234
|
if (isEmptyRules(rules)) return [...argv];
|
|
334
235
|
const dialect = dialectOf(argv);
|
|
@@ -342,9 +243,7 @@ function extendConfinedArgv(argv, rules) {
|
|
|
342
243
|
}
|
|
343
244
|
//#endregion
|
|
344
245
|
//#region src/containment.ts
|
|
345
|
-
/** 视为“路径不存在”的错误码:只有它们能让祖先遍历继续。 */
|
|
346
246
|
const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
347
|
-
/** stat 一次,缺失返回 undefined;其它失败(权限、I/O)继续抛出。 */
|
|
348
247
|
async function statIfPresent(path) {
|
|
349
248
|
try {
|
|
350
249
|
return await stat(path, { bigint: true });
|
|
@@ -354,11 +253,9 @@ async function statIfPresent(path) {
|
|
|
354
253
|
throw error;
|
|
355
254
|
}
|
|
356
255
|
}
|
|
357
|
-
/** 按大小写敏感性归一用于比较的拼写。 */
|
|
358
256
|
function comparablePath(path, caseSensitive) {
|
|
359
257
|
return caseSensitive ? path : path.toLowerCase();
|
|
360
258
|
}
|
|
361
|
-
/** 词法包含判定(目标可带尚不存在的后缀)。 */
|
|
362
259
|
function isLexicallyUnder(path, root, caseSensitive) {
|
|
363
260
|
const comparableTarget = comparablePath(path, caseSensitive);
|
|
364
261
|
const comparableRoot = comparablePath(root, caseSensitive);
|
|
@@ -366,19 +263,9 @@ function isLexicallyUnder(path, root, caseSensitive) {
|
|
|
366
263
|
const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
|
|
367
264
|
return comparableTarget.startsWith(prefix);
|
|
368
265
|
}
|
|
369
|
-
/** 两次 stat 是否指向同一个文件系统对象。 */
|
|
370
266
|
function sameIdentity(left, right) {
|
|
371
267
|
return left.dev === right.dev && left.ino === right.ino;
|
|
372
268
|
}
|
|
373
|
-
/**
|
|
374
|
-
* 判断 canonical 目标是否就是某个根或位于其下。
|
|
375
|
-
* 拼写不同时(Windows 长名/8.3 别名、大小写差异)沿目标的现存祖先比较文件系统身份,
|
|
376
|
-
* 不把包含判定弱化成文本近似。
|
|
377
|
-
* @param path - 目标的 canonical 路径(可带尚不存在的尾部)。
|
|
378
|
-
* @param root - canonical 可写根。
|
|
379
|
-
* @param caseSensitive - 词法比较是否区分大小写;默认按宿主平台约定。
|
|
380
|
-
* @returns 目标是该根或其后代时为 true。
|
|
381
|
-
*/
|
|
382
269
|
async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
|
|
383
270
|
if (isLexicallyUnder(path, root, caseSensitive)) return true;
|
|
384
271
|
const rootInfo = await statIfPresent(root);
|
|
@@ -394,78 +281,39 @@ async function isPathUnder(path, root, caseSensitive = process.platform !== "win
|
|
|
394
281
|
}
|
|
395
282
|
//#endregion
|
|
396
283
|
//#region src/fs.ts
|
|
397
|
-
/**
|
|
398
|
-
* 官方本机文件系统后端的可配置版本。
|
|
399
|
-
* `--` 条目在解析阶段拒绝目标(覆盖 read / write / edit / list 等一切工具入口),
|
|
400
|
-
* `r-` 与 `--` 条目在写入前拒绝写入(优先于任何可写根),写操作再按
|
|
401
|
-
* `workspace-write` + `rw` 条目复核 containment。
|
|
402
|
-
*/
|
|
403
284
|
var ConfigurableFileSystem = class extends LocalFileSystem {
|
|
404
285
|
static inject = ["sandboxPolicy"];
|
|
405
286
|
defaultMode;
|
|
406
287
|
source;
|
|
407
|
-
/** 规则按工作区根编译一次(相对规则相对该调用的工作区)。 */
|
|
408
288
|
compiled = /* @__PURE__ */ new Map();
|
|
409
289
|
constructor(ctx, config) {
|
|
410
290
|
super(ctx, config);
|
|
411
291
|
this.defaultMode = ctx.sandboxPolicy.defaultMode;
|
|
412
292
|
this.source = ruleSourceOf(config, process.env);
|
|
413
293
|
}
|
|
414
|
-
/** 工具层读它判断后端是否 confine(并据此广告 escalation 字段)。 */
|
|
415
294
|
get sandboxMode() {
|
|
416
295
|
return this.defaultMode;
|
|
417
296
|
}
|
|
418
|
-
/**
|
|
419
|
-
* 解析目标后立即执行拒绝判定:工具入口(read / write / edit / list)都先经过
|
|
420
|
-
* `resolve`,因此一次判定即可覆盖读与写。
|
|
421
|
-
* @param path - 待解析的路径。
|
|
422
|
-
* @param opts - cwd 与取消信号;cwd 同时是相对规则的解析根。
|
|
423
|
-
* @returns 解析后的目标。
|
|
424
|
-
*/
|
|
425
297
|
async resolve(path, opts) {
|
|
426
298
|
const target = await super.resolve(path, opts);
|
|
427
299
|
this.assertNotDenied(this.rulesFor(opts?.cwd ?? this.ctx.sandboxPolicy.workspaceRoot), target.targetKey, target.displayPath);
|
|
428
300
|
return target;
|
|
429
301
|
}
|
|
430
|
-
/**
|
|
431
|
-
* 按 per-call 策略复核后写入。
|
|
432
|
-
* @param target - 工具解析出的目标。
|
|
433
|
-
* @param content - 新的完整内容。
|
|
434
|
-
* @param expected - 写入前版本守卫。
|
|
435
|
-
* @param signal - 取消信号。
|
|
436
|
-
* @param sandboxPolicy - per-call 策略;省略时用部署默认。
|
|
437
|
-
* @returns 上游写入结果。
|
|
438
|
-
*/
|
|
439
302
|
async writeText(target, content, expected, signal, sandboxPolicy) {
|
|
440
303
|
return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
|
|
441
304
|
}
|
|
442
|
-
/**
|
|
443
|
-
* 按 per-call 策略复核后编辑。
|
|
444
|
-
* @param target - 工具解析出的目标。
|
|
445
|
-
* @param edit - 字面量 search/replace 请求。
|
|
446
|
-
* @param expected - 版本守卫。
|
|
447
|
-
* @param signal - 取消信号。
|
|
448
|
-
* @param sandboxPolicy - per-call 策略;省略时用部署默认。
|
|
449
|
-
* @returns 上游编辑结果。
|
|
450
|
-
*/
|
|
451
305
|
async editText(target, edit, expected, signal, sandboxPolicy) {
|
|
452
306
|
return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
|
|
453
307
|
}
|
|
454
|
-
/** `--` 条目命中即拒绝访问(读与写都拒);抛 `FS_SANDBOX_DENIED`。 */
|
|
455
308
|
assertNotDenied(rules, canonicalTarget, displayPath) {
|
|
456
309
|
if (!isDenied(rules, canonicalTarget)) return;
|
|
457
310
|
throw new FsError(`cannot access "${displayPath}": file access denied by the configured "--" rules`, "FS_SANDBOX_DENIED");
|
|
458
311
|
}
|
|
459
|
-
/** `--` 或 `r-` 条目命中即拒绝写入,且优先于任何可写根。 */
|
|
460
312
|
assertWritable(rules, canonicalTarget, displayPath) {
|
|
461
313
|
this.assertNotDenied(rules, canonicalTarget, displayPath);
|
|
462
314
|
if (!isReadOnly(rules, canonicalTarget)) return;
|
|
463
315
|
throw new FsError(`cannot write "${displayPath}": path is read-only by the configured "r-" rule`, "FS_SANDBOX_DENIED");
|
|
464
316
|
}
|
|
465
|
-
/**
|
|
466
|
-
* 写前复核:`--` / `r-` 条目(任何模式)→ 模式本身的只读拒绝 → `workspace-write` 的
|
|
467
|
-
* `writableRoots + rw` containment,返回必须被写入的那个目标。
|
|
468
|
-
*/
|
|
469
317
|
async checkedTarget(target, sandboxPolicy) {
|
|
470
318
|
const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
471
319
|
const rules = this.rulesFor(policy.workspaceRoot);
|
|
@@ -478,7 +326,6 @@ var ConfigurableFileSystem = class extends LocalFileSystem {
|
|
|
478
326
|
for (const root of [...writableRoots(policy), ...rules.allowRoots]) if (await isPathUnder(fresh.targetKey, root)) return fresh;
|
|
479
327
|
throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
|
|
480
328
|
}
|
|
481
|
-
/** 取(并按需编译缓存)某个工作区根下的规则。 */
|
|
482
329
|
rulesFor(workspaceRoot) {
|
|
483
330
|
const cached = this.compiled.get(workspaceRoot);
|
|
484
331
|
if (cached !== void 0) return cached;
|
|
@@ -489,27 +336,15 @@ var ConfigurableFileSystem = class extends LocalFileSystem {
|
|
|
489
336
|
};
|
|
490
337
|
//#endregion
|
|
491
338
|
//#region src/sandbox.ts
|
|
492
|
-
/**
|
|
493
|
-
* 官方本机沙箱 provider 的可配置版本。
|
|
494
|
-
* `read-only` 模式不追加 `rw` 条目(只读边界不因额外可写根放松);`--` 条目在两种
|
|
495
|
-
* confined 模式下都生效。
|
|
496
|
-
*/
|
|
497
339
|
var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
|
|
498
340
|
source;
|
|
499
|
-
/** 规则按工作区根编译一次(相对规则相对该调用的工作区)。 */
|
|
500
341
|
compiled = /* @__PURE__ */ new Map();
|
|
501
342
|
constructor(ctx, config) {
|
|
502
343
|
super(ctx, config);
|
|
503
344
|
this.source = ruleSourceOf(config, process.env);
|
|
504
345
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
* @param argv - 调用方即将 spawn 的 argv。
|
|
508
|
-
* @param policy - 本次调用的文件效果策略。
|
|
509
|
-
* @returns 追加规则后的 confined argv(空规则时与官方结果一致)。
|
|
510
|
-
*/
|
|
511
|
-
confine(argv, policy) {
|
|
512
|
-
const confined = super.confine(argv, policy);
|
|
346
|
+
async confine(argv, policy, signal) {
|
|
347
|
+
const confined = await super.confine(argv, policy, signal);
|
|
513
348
|
const rules = this.rulesFor(policy.workspaceRoot);
|
|
514
349
|
const effective = policy.mode === "workspace-write" ? rules : withoutAllowRoots(rules);
|
|
515
350
|
if (isEmptyRules(effective)) return confined;
|
|
@@ -518,7 +353,6 @@ var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
|
|
|
518
353
|
argv: extendConfinedArgv(confined.argv, effective)
|
|
519
354
|
};
|
|
520
355
|
}
|
|
521
|
-
/** 取(并按需编译缓存)某个工作区根下的规则。 */
|
|
522
356
|
rulesFor(workspaceRoot) {
|
|
523
357
|
const cached = this.compiled.get(workspaceRoot);
|
|
524
358
|
if (cached !== void 0) return cached;
|
|
@@ -529,15 +363,6 @@ var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
|
|
|
529
363
|
};
|
|
530
364
|
//#endregion
|
|
531
365
|
//#region src/config.ts
|
|
532
|
-
/**
|
|
533
|
-
* 插件配置的单一真源:`access` 规则 + 透传官方的字段。
|
|
534
|
-
*
|
|
535
|
-
* 规则只在这里声明一次——provider 与 fs 后端按构造参数接收已解析的配置,
|
|
536
|
-
* 不各自重复声明 schema;部署层的规则值随插入本行的 patch 写死(本部署见
|
|
537
|
-
* `@morlay/dsh-preset`),包内不预设。
|
|
538
|
-
* @module @morlay/dsh-sandbox-local/config
|
|
539
|
-
*/
|
|
540
|
-
/** 运行时配置 schema。 */
|
|
541
366
|
const Config = z.object({
|
|
542
367
|
access: z.union([z.array(z.string()), z.string()]).default([]),
|
|
543
368
|
runnerCommand: z.array(z.string()).default([]),
|
|
@@ -548,15 +373,8 @@ const Config = z.object({
|
|
|
548
373
|
});
|
|
549
374
|
//#endregion
|
|
550
375
|
//#region src/index.ts
|
|
551
|
-
/** Cordis 插件名。 */
|
|
552
376
|
const name = "sandbox-local";
|
|
553
|
-
/** fs 侧从策略服务取默认模式与工作区回退根,所以先等 `ctx.sandboxPolicy`。 */
|
|
554
377
|
const inject = ["sandboxPolicy"];
|
|
555
|
-
/**
|
|
556
|
-
* 规则在进程沙箱侧的能力随平台方言变化,加载期把降级说清楚:
|
|
557
|
-
* `ctx.fs` 侧(read / write / edit 工具)在 macOS / Linux / Windows 上语义一致,
|
|
558
|
-
* 只有 bash 等子进程走平台 runner 的表达能力。
|
|
559
|
-
*/
|
|
560
378
|
function warnAboutDegradedRules(ctx, config) {
|
|
561
379
|
const rules = ruleSourceOf(config, process.env);
|
|
562
380
|
const grants = rules.allowWrite.length > 0;
|
|
@@ -571,11 +389,6 @@ function warnAboutDegradedRules(ctx, config) {
|
|
|
571
389
|
}
|
|
572
390
|
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");
|
|
573
391
|
}
|
|
574
|
-
/**
|
|
575
|
-
* 注册两个替换实现。
|
|
576
|
-
* @param ctx - 插件上下文(官方 `sandbox` / `fs-sandbox` 行已禁用)。
|
|
577
|
-
* @param config - 已由 schema 填好默认值的配置。
|
|
578
|
-
*/
|
|
579
392
|
function apply(ctx, config) {
|
|
580
393
|
warnAboutDegradedRules(ctx, config);
|
|
581
394
|
new ConfigurableSandboxProvider(ctx, config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-sandbox-local",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-alpha.0",
|
|
4
4
|
"description": "Configurable sandbox bundle: replaces the shipped process-sandbox provider and filesystem fence with implementations that add extra writable roots and access denials on top of the upstream semantics.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "https://github.com/morlay/
|
|
15
|
+
"url": "https://github.com/morlay/dsh-plugin.git"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
@@ -28,21 +28,21 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
31
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
32
|
-
"@deepseek-ai/dsh-fs-local": "^0.1.
|
|
33
|
-
"@deepseek-ai/dsh-fs-sandbox": "^0.1.
|
|
34
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
35
|
-
"@deepseek-ai/dsh-sandbox-local": "^0.1.
|
|
36
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.1.
|
|
31
|
+
"@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-fs-local": "^0.1.6-alpha.2",
|
|
33
|
+
"@deepseek-ai/dsh-fs-sandbox": "^0.1.6-alpha.2",
|
|
34
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
|
|
35
|
+
"@deepseek-ai/dsh-sandbox-local": "^0.1.6-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
40
|
-
"@deepseek-ai/dsh-fs": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-fs-local": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-fs-sandbox": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-sandbox-local": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.1.
|
|
40
|
+
"@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-fs-local": "^0.1.6-alpha.2",
|
|
42
|
+
"@deepseek-ai/dsh-fs-sandbox": "^0.1.6-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.6-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-sandbox-local": "^0.1.6-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.1.6-alpha.2",
|
|
46
46
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
47
47
|
},
|
|
48
48
|
"dsh": {
|
package/src/config.ts
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 插件配置的单一真源:`access` 规则 + 透传官方的字段。
|
|
3
|
-
*
|
|
4
|
-
* 规则只在这里声明一次——provider 与 fs 后端按构造参数接收已解析的配置,
|
|
5
|
-
* 不各自重复声明 schema;部署层的规则值随插入本行的 patch 写死(本部署见
|
|
6
|
-
* `@morlay/dsh-preset`),包内不预设。
|
|
7
|
-
* @module @morlay/dsh-sandbox-local/config
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import z from "@deepseek-ai/schemastery";
|
|
11
2
|
import type { Config as UpstreamFsConfig } from "@deepseek-ai/dsh-fs-local";
|
|
12
3
|
import type { Config as UpstreamSandboxConfig } from "@deepseek-ai/dsh-sandbox-local";
|
|
13
4
|
|
|
14
|
-
/** 插件配置。 */
|
|
15
5
|
export interface Config extends UpstreamSandboxConfig, UpstreamFsConfig {
|
|
16
|
-
/**
|
|
17
|
-
* 规则条目:`rw <path>`(额外可写根)、`r- <path>`(只读)、`-- <pattern>`(拒绝访问)。
|
|
18
|
-
* 数组每项一条,或写一段多行文本(每行一条);`{{ env.NAME }}` 按进程环境展开。
|
|
19
|
-
*/
|
|
20
6
|
access?: string | string[];
|
|
21
7
|
}
|
|
22
8
|
|
|
23
|
-
/** 运行时配置 schema。 */
|
|
24
9
|
export const Config: z<Config> = z.object({
|
|
25
10
|
access: z.union([z.array(z.string()), z.string()]).default([]),
|
|
26
11
|
runnerCommand: z.array(z.string()).default([]),
|
package/src/containment.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 目标是否位于某个可写根之下的判定。
|
|
3
|
-
*
|
|
4
|
-
* 语义与上游 `@deepseek-ai/dsh-fs-sandbox/src/containment.ts` 对齐(canonical 拼写走
|
|
5
|
-
* 词法快路径,别名/大小写差异由文件系统身份兜底);本包自带一份,是因为该模块只存在于
|
|
6
|
-
* 上游包的 `src/` 深处,而发布产物(`lib/`)与安装态 profile 的模块解析都不覆盖深路径。
|
|
7
|
-
* @module @morlay/dsh-sandbox-local/containment
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import type { BigIntStats } from "node:fs";
|
|
11
2
|
import { stat } from "node:fs/promises";
|
|
12
3
|
import { dirname, sep } from "node:path";
|
|
13
4
|
|
|
14
|
-
/** 视为“路径不存在”的错误码:只有它们能让祖先遍历继续。 */
|
|
15
5
|
const MISSING_CODES: ReadonlySet<string> = new Set(["ENOENT", "ENOTDIR"]);
|
|
16
6
|
|
|
17
|
-
/** stat 一次,缺失返回 undefined;其它失败(权限、I/O)继续抛出。 */
|
|
18
7
|
async function statIfPresent(path: string): Promise<BigIntStats | undefined> {
|
|
19
8
|
try {
|
|
20
9
|
return await stat(path, { bigint: true });
|
|
@@ -25,12 +14,10 @@ async function statIfPresent(path: string): Promise<BigIntStats | undefined> {
|
|
|
25
14
|
}
|
|
26
15
|
}
|
|
27
16
|
|
|
28
|
-
/** 按大小写敏感性归一用于比较的拼写。 */
|
|
29
17
|
function comparablePath(path: string, caseSensitive: boolean): string {
|
|
30
18
|
return caseSensitive ? path : path.toLowerCase();
|
|
31
19
|
}
|
|
32
20
|
|
|
33
|
-
/** 词法包含判定(目标可带尚不存在的后缀)。 */
|
|
34
21
|
function isLexicallyUnder(path: string, root: string, caseSensitive: boolean): boolean {
|
|
35
22
|
const comparableTarget = comparablePath(path, caseSensitive);
|
|
36
23
|
const comparableRoot = comparablePath(root, caseSensitive);
|
|
@@ -39,20 +26,10 @@ function isLexicallyUnder(path: string, root: string, caseSensitive: boolean): b
|
|
|
39
26
|
return comparableTarget.startsWith(prefix);
|
|
40
27
|
}
|
|
41
28
|
|
|
42
|
-
/** 两次 stat 是否指向同一个文件系统对象。 */
|
|
43
29
|
function sameIdentity(left: BigIntStats, right: BigIntStats): boolean {
|
|
44
30
|
return left.dev === right.dev && left.ino === right.ino;
|
|
45
31
|
}
|
|
46
32
|
|
|
47
|
-
/**
|
|
48
|
-
* 判断 canonical 目标是否就是某个根或位于其下。
|
|
49
|
-
* 拼写不同时(Windows 长名/8.3 别名、大小写差异)沿目标的现存祖先比较文件系统身份,
|
|
50
|
-
* 不把包含判定弱化成文本近似。
|
|
51
|
-
* @param path - 目标的 canonical 路径(可带尚不存在的尾部)。
|
|
52
|
-
* @param root - canonical 可写根。
|
|
53
|
-
* @param caseSensitive - 词法比较是否区分大小写;默认按宿主平台约定。
|
|
54
|
-
* @returns 目标是该根或其后代时为 true。
|
|
55
|
-
*/
|
|
56
33
|
export async function isPathUnder(
|
|
57
34
|
path: string,
|
|
58
35
|
root: string,
|
package/src/dialects.ts
CHANGED
|
@@ -1,40 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 在官方 provider 生成的 confined argv 上追加本包规则:
|
|
3
|
-
* Seatbelt 追加 SBPL 规则(后置规则覆盖先前的 allow,实测 `(deny file-read* file-write*
|
|
4
|
-
* (subpath …))` 能压过 `(allow file-write* (subpath …))`,`r-` 条目则只 deny 写入),
|
|
5
|
-
* bwrap 追加挂载参数(`rw` 用 `--bind-try`,`r-` 与 `--` 都用 `--ro-bind-try`),
|
|
6
|
-
* Landlock 只能追加可写授权(其 allow-list 语义无法减除子路径),
|
|
7
|
-
* Windows ACL runner 没有对应表达。
|
|
8
|
-
*
|
|
9
|
-
* 方言从官方 `ConfinedArgv.argv` 的结构识别:runner 参数在前,`--` 之后是调用方 argv。
|
|
10
|
-
* @module @morlay/dsh-sandbox-local/dialects
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
import { isEmptyRules, type CompiledRules } from "./rules.ts";
|
|
14
2
|
|
|
15
|
-
/** 官方 provider 可选的执行方言。 */
|
|
16
3
|
export type SandboxDialect = "seatbelt" | "bwrap" | "landlock" | "windows-acl";
|
|
17
4
|
|
|
18
|
-
/** runner 部分与调用方 argv 的分隔符。 */
|
|
19
5
|
const SEPARATOR = "--";
|
|
20
6
|
|
|
21
|
-
/**
|
|
22
|
-
* 一个方言能表达的规则能力。
|
|
23
|
-
* `readOnly` 指 `r-` 条目(只拒写入);`denyWriteOnly` 指 `--` 条目在该方言上只能
|
|
24
|
-
* 退化为“只拒写入”(读取仍放行)。
|
|
25
|
-
*/
|
|
26
7
|
export interface DialectCapabilities {
|
|
27
|
-
/** 能否把额外可写根写进 runner 参数。 */
|
|
28
8
|
readonly allowWrite: boolean;
|
|
29
|
-
|
|
9
|
+
|
|
30
10
|
readonly readOnly: boolean;
|
|
31
|
-
|
|
11
|
+
|
|
32
12
|
readonly denyReadWrite: boolean;
|
|
33
|
-
|
|
13
|
+
|
|
34
14
|
readonly denyWriteOnly: boolean;
|
|
35
15
|
}
|
|
36
16
|
|
|
37
|
-
/** 各方言的规则表达能力——加载期据此告警,运行期据此决定是否改写参数。 */
|
|
38
17
|
export const DIALECT_CAPABILITIES: Record<SandboxDialect, DialectCapabilities> = {
|
|
39
18
|
seatbelt: { allowWrite: true, readOnly: true, denyReadWrite: true, denyWriteOnly: false },
|
|
40
19
|
bwrap: { allowWrite: true, readOnly: true, denyReadWrite: false, denyWriteOnly: true },
|
|
@@ -42,7 +21,6 @@ export const DIALECT_CAPABILITIES: Record<SandboxDialect, DialectCapabilities> =
|
|
|
42
21
|
"windows-acl": { allowWrite: false, readOnly: false, denyReadWrite: false, denyWriteOnly: false },
|
|
43
22
|
};
|
|
44
23
|
|
|
45
|
-
/** runner 部分的结束位置。 */
|
|
46
24
|
function separatorIndex(argv: readonly string[]): number {
|
|
47
25
|
const index = argv.indexOf(SEPARATOR);
|
|
48
26
|
if (index === -1) {
|
|
@@ -51,11 +29,6 @@ function separatorIndex(argv: readonly string[]): number {
|
|
|
51
29
|
return index;
|
|
52
30
|
}
|
|
53
31
|
|
|
54
|
-
/**
|
|
55
|
-
* 识别 argv 使用的执行方言。
|
|
56
|
-
* @param argv - 官方 provider 返回的完整 confined argv。
|
|
57
|
-
* @returns 方言,或无法识别时的 `undefined`(例如运维自定义的 runnerCommand)。
|
|
58
|
-
*/
|
|
59
32
|
export function dialectOf(argv: readonly string[]): SandboxDialect | undefined {
|
|
60
33
|
const separator = argv.indexOf(SEPARATOR);
|
|
61
34
|
const runner = separator === -1 ? argv : argv.slice(0, separator);
|
|
@@ -66,15 +39,10 @@ export function dialectOf(argv: readonly string[]): SandboxDialect | undefined {
|
|
|
66
39
|
return undefined;
|
|
67
40
|
}
|
|
68
41
|
|
|
69
|
-
/** SBPL 字符串字面量。 */
|
|
70
42
|
function sbplString(value: string): string {
|
|
71
43
|
return `"${value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
|
|
72
44
|
}
|
|
73
45
|
|
|
74
|
-
/**
|
|
75
|
-
* SBPL `#"…"` 的正则体:正则自身的 `\` 必须保留(glob 翻译用它转义元字符),
|
|
76
|
-
* 因此拒绝项里出现双引号时直接报错,而不是产出一个含义变化的 profile。
|
|
77
|
-
*/
|
|
78
46
|
function sbplRegexBody(source: string): string {
|
|
79
47
|
if (source.includes('"')) {
|
|
80
48
|
throw new Error(
|
|
@@ -84,7 +52,6 @@ function sbplRegexBody(source: string): string {
|
|
|
84
52
|
return source;
|
|
85
53
|
}
|
|
86
54
|
|
|
87
|
-
/** Seatbelt:把 allow / deny 规则追加到 `-p` 的 profile 文本末尾。 */
|
|
88
55
|
function extendSeatbelt(argv: readonly string[], rules: CompiledRules): string[] {
|
|
89
56
|
const index = argv.indexOf("-p");
|
|
90
57
|
const profile = index === -1 ? undefined : argv[index + 1];
|
|
@@ -108,11 +75,6 @@ function extendSeatbelt(argv: readonly string[], rules: CompiledRules): string[]
|
|
|
108
75
|
return [...argv.slice(0, index + 1), extended, ...argv.slice(index + 2)];
|
|
109
76
|
}
|
|
110
77
|
|
|
111
|
-
/**
|
|
112
|
-
* bwrap:额外可写根用 `--bind-try`(路径不存在时跳过);`r-` 与 `--` 都用
|
|
113
|
-
* `--ro-bind-try` 覆盖成只读(挂载后行覆盖前行)——也就是说 `--` 在 bwrap 上退化为
|
|
114
|
-
* “只拒写入”。glob 模式无法表达为静态挂载,由调用方在加载期告警。
|
|
115
|
-
*/
|
|
116
78
|
function extendBwrap(argv: readonly string[], rules: CompiledRules): string[] {
|
|
117
79
|
const separator = separatorIndex(argv);
|
|
118
80
|
const additions: string[] = [];
|
|
@@ -123,19 +85,12 @@ function extendBwrap(argv: readonly string[], rules: CompiledRules): string[] {
|
|
|
123
85
|
return [...argv.slice(0, separator), ...additions, ...argv.slice(separator)];
|
|
124
86
|
}
|
|
125
87
|
|
|
126
|
-
/** Landlock:只能追加可写授权(`--rw`),`r-` 与 `--` 都无对应表达。 */
|
|
127
88
|
function extendLandlock(argv: readonly string[], rules: CompiledRules): string[] {
|
|
128
89
|
const separator = separatorIndex(argv);
|
|
129
90
|
const additions = rules.allowRoots.flatMap((root) => ["--rw", root]);
|
|
130
91
|
return [...argv.slice(0, separator), ...additions, ...argv.slice(separator)];
|
|
131
92
|
}
|
|
132
93
|
|
|
133
|
-
/**
|
|
134
|
-
* 按 argv 的方言追加规则。
|
|
135
|
-
* @param argv - 官方 provider 返回的 confined argv。
|
|
136
|
-
* @param rules - 已编译规则;空规则原样返回。
|
|
137
|
-
* @returns 追加规则后的 argv;方言无法识别时抛错(规则不能静默失效)。
|
|
138
|
-
*/
|
|
139
94
|
export function extendConfinedArgv(argv: readonly string[], rules: CompiledRules): string[] {
|
|
140
95
|
if (isEmptyRules(rules)) return [...argv];
|
|
141
96
|
const dialect = dialectOf(argv);
|
|
@@ -152,7 +107,6 @@ export function extendConfinedArgv(argv: readonly string[], rules: CompiledRules
|
|
|
152
107
|
case "landlock":
|
|
153
108
|
return extendLandlock(argv, rules);
|
|
154
109
|
case "windows-acl":
|
|
155
|
-
// windows-acl 的 runner 参数没有承载额外 grant 的入口:加载期已告警,这里保持原 argv。
|
|
156
110
|
return [...argv];
|
|
157
111
|
}
|
|
158
112
|
}
|
package/src/fs.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 文件系统围栏:继承官方 `@deepseek-ai/dsh-fs-local` 的文本存储机制
|
|
3
|
-
* (resolve / stat / 读流 / 原子写 / read-match-write 编辑),在访问入口叠加规则:
|
|
4
|
-
* `--` 命中即拒绝访问(读与写都拒,任何模式下都生效),`r-` 只拒写入,`rw` 参与
|
|
5
|
-
* `workspace-write` 的可写判定(官方 `writableRoots` 之外的额外可写根)。
|
|
6
|
-
*
|
|
7
|
-
* 与它替换掉的官方 `@deepseek-ai/dsh-fs-sandbox` 一样,这是受信代码里的策略检查,
|
|
8
|
-
* 不是内核边界:内核级隔离仍由 `ctx.sandbox` 侧负责。
|
|
9
|
-
* @module @morlay/dsh-sandbox-local/fs
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
13
2
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
14
3
|
import type {
|
|
@@ -34,18 +23,12 @@ import {
|
|
|
34
23
|
type RuleSource,
|
|
35
24
|
} from "./rules.ts";
|
|
36
25
|
|
|
37
|
-
/**
|
|
38
|
-
* 官方本机文件系统后端的可配置版本。
|
|
39
|
-
* `--` 条目在解析阶段拒绝目标(覆盖 read / write / edit / list 等一切工具入口),
|
|
40
|
-
* `r-` 与 `--` 条目在写入前拒绝写入(优先于任何可写根),写操作再按
|
|
41
|
-
* `workspace-write` + `rw` 条目复核 containment。
|
|
42
|
-
*/
|
|
43
26
|
export class ConfigurableFileSystem extends LocalFileSystem {
|
|
44
27
|
static inject = ["sandboxPolicy"];
|
|
45
28
|
|
|
46
29
|
private readonly defaultMode: SandboxMode;
|
|
47
30
|
private readonly source: RuleSource;
|
|
48
|
-
|
|
31
|
+
|
|
49
32
|
private readonly compiled = new Map<string, CompiledRules>();
|
|
50
33
|
|
|
51
34
|
constructor(ctx: Context, config: Config) {
|
|
@@ -54,18 +37,10 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
54
37
|
this.source = ruleSourceOf(config, process.env);
|
|
55
38
|
}
|
|
56
39
|
|
|
57
|
-
/** 工具层读它判断后端是否 confine(并据此广告 escalation 字段)。 */
|
|
58
40
|
override get sandboxMode(): SandboxMode {
|
|
59
41
|
return this.defaultMode;
|
|
60
42
|
}
|
|
61
43
|
|
|
62
|
-
/**
|
|
63
|
-
* 解析目标后立即执行拒绝判定:工具入口(read / write / edit / list)都先经过
|
|
64
|
-
* `resolve`,因此一次判定即可覆盖读与写。
|
|
65
|
-
* @param path - 待解析的路径。
|
|
66
|
-
* @param opts - cwd 与取消信号;cwd 同时是相对规则的解析根。
|
|
67
|
-
* @returns 解析后的目标。
|
|
68
|
-
*/
|
|
69
44
|
override async resolve(
|
|
70
45
|
path: string,
|
|
71
46
|
opts?: { cwd?: string; signal?: AbortSignal },
|
|
@@ -79,15 +54,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
79
54
|
return target;
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/**
|
|
83
|
-
* 按 per-call 策略复核后写入。
|
|
84
|
-
* @param target - 工具解析出的目标。
|
|
85
|
-
* @param content - 新的完整内容。
|
|
86
|
-
* @param expected - 写入前版本守卫。
|
|
87
|
-
* @param signal - 取消信号。
|
|
88
|
-
* @param sandboxPolicy - per-call 策略;省略时用部署默认。
|
|
89
|
-
* @returns 上游写入结果。
|
|
90
|
-
*/
|
|
91
57
|
override async writeText(
|
|
92
58
|
target: FsTarget,
|
|
93
59
|
content: string,
|
|
@@ -103,15 +69,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
103
69
|
);
|
|
104
70
|
}
|
|
105
71
|
|
|
106
|
-
/**
|
|
107
|
-
* 按 per-call 策略复核后编辑。
|
|
108
|
-
* @param target - 工具解析出的目标。
|
|
109
|
-
* @param edit - 字面量 search/replace 请求。
|
|
110
|
-
* @param expected - 版本守卫。
|
|
111
|
-
* @param signal - 取消信号。
|
|
112
|
-
* @param sandboxPolicy - per-call 策略;省略时用部署默认。
|
|
113
|
-
* @returns 上游编辑结果。
|
|
114
|
-
*/
|
|
115
72
|
override async editText(
|
|
116
73
|
target: FsTarget,
|
|
117
74
|
edit: FsEditRequest,
|
|
@@ -122,7 +79,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
122
79
|
return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
|
|
123
80
|
}
|
|
124
81
|
|
|
125
|
-
/** `--` 条目命中即拒绝访问(读与写都拒);抛 `FS_SANDBOX_DENIED`。 */
|
|
126
82
|
private assertNotDenied(
|
|
127
83
|
rules: CompiledRules,
|
|
128
84
|
canonicalTarget: string,
|
|
@@ -135,7 +91,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
135
91
|
);
|
|
136
92
|
}
|
|
137
93
|
|
|
138
|
-
/** `--` 或 `r-` 条目命中即拒绝写入,且优先于任何可写根。 */
|
|
139
94
|
private assertWritable(rules: CompiledRules, canonicalTarget: string, displayPath: string): void {
|
|
140
95
|
this.assertNotDenied(rules, canonicalTarget, displayPath);
|
|
141
96
|
if (!isReadOnly(rules, canonicalTarget)) return;
|
|
@@ -145,10 +100,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
145
100
|
);
|
|
146
101
|
}
|
|
147
102
|
|
|
148
|
-
/**
|
|
149
|
-
* 写前复核:`--` / `r-` 条目(任何模式)→ 模式本身的只读拒绝 → `workspace-write` 的
|
|
150
|
-
* `writableRoots + rw` containment,返回必须被写入的那个目标。
|
|
151
|
-
*/
|
|
152
103
|
private async checkedTarget(
|
|
153
104
|
target: FsTarget,
|
|
154
105
|
sandboxPolicy?: SandboxExecutionPolicy,
|
|
@@ -164,8 +115,7 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
164
115
|
"FS_SANDBOX_DENIED",
|
|
165
116
|
);
|
|
166
117
|
}
|
|
167
|
-
|
|
168
|
-
// “检查这里、写那里”的 TOCTOU 窗口)。
|
|
118
|
+
|
|
169
119
|
const fresh = await super.resolve(target.displayPath);
|
|
170
120
|
this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
|
|
171
121
|
for (const root of [...writableRoots(policy), ...rules.allowRoots]) {
|
|
@@ -177,7 +127,6 @@ export class ConfigurableFileSystem extends LocalFileSystem {
|
|
|
177
127
|
);
|
|
178
128
|
}
|
|
179
129
|
|
|
180
|
-
/** 取(并按需编译缓存)某个工作区根下的规则。 */
|
|
181
130
|
private rulesFor(workspaceRoot: string): CompiledRules {
|
|
182
131
|
const cached = this.compiled.get(workspaceRoot);
|
|
183
132
|
if (cached !== undefined) return cached;
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 可配置沙箱插件:替换官方进程沙箱 provider(`ctx.sandbox`)与文件系统后端(`ctx.fs`),
|
|
3
|
-
* 在官方语义之上叠加 `access` 规则。
|
|
4
|
-
*
|
|
5
|
-
* 每条规则以 `rw ` / `r- ` / `-- ` 开头:`rw <path>` 是工作区与平台临时目录之外的额外
|
|
6
|
-
* 可写根,`r- <path>` 是只读(读放行、写拒绝),`-- <pattern>` 是访问拒绝(读 + 写)。
|
|
7
|
-
* 三条都支持 `{{ env.NAME }}`(加载期展开,引用未定义的环境变量直接失败)与相对工作区的
|
|
8
|
-
* 路径;命中优先级是 `--` > `r-` > 可写根。`ctx.fs` 侧语义完整,`ctx.sandbox` 侧在能表达
|
|
9
|
-
* 该语义的方言上生效(见 {@link DIALECT_CAPABILITIES})。
|
|
10
|
-
*
|
|
11
|
-
* 装配前提:官方 `sandbox` 与 `fs-sandbox` 行必须被禁用(包内 `cordis.patch.yml` 或
|
|
12
|
-
* 部署层 patch),否则同名服务 fail loud;规则由部署层在插入本行时写死
|
|
13
|
-
* (本部署见 `@morlay/dsh-preset`),包内不预设。
|
|
14
|
-
* @module @morlay/dsh-sandbox-local
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
18
2
|
import type { Config } from "./config.ts";
|
|
19
3
|
import { DIALECT_CAPABILITIES } from "./dialects.ts";
|
|
@@ -21,20 +5,12 @@ import { ConfigurableFileSystem } from "./fs.ts";
|
|
|
21
5
|
import { ruleSourceOf } from "./rules.ts";
|
|
22
6
|
import { ConfigurableSandboxProvider } from "./sandbox.ts";
|
|
23
7
|
|
|
24
|
-
// 值与类型一起转出:Loader 读 `Config`(schema),消费方读类型。
|
|
25
8
|
export { Config } from "./config.ts";
|
|
26
9
|
|
|
27
|
-
/** Cordis 插件名。 */
|
|
28
10
|
export const name = "sandbox-local";
|
|
29
11
|
|
|
30
|
-
/** fs 侧从策略服务取默认模式与工作区回退根,所以先等 `ctx.sandboxPolicy`。 */
|
|
31
12
|
export const inject = ["sandboxPolicy"];
|
|
32
13
|
|
|
33
|
-
/**
|
|
34
|
-
* 规则在进程沙箱侧的能力随平台方言变化,加载期把降级说清楚:
|
|
35
|
-
* `ctx.fs` 侧(read / write / edit 工具)在 macOS / Linux / Windows 上语义一致,
|
|
36
|
-
* 只有 bash 等子进程走平台 runner 的表达能力。
|
|
37
|
-
*/
|
|
38
14
|
function warnAboutDegradedRules(ctx: Context, config: Config): void {
|
|
39
15
|
const rules = ruleSourceOf(config, process.env);
|
|
40
16
|
const grants = rules.allowWrite.length > 0;
|
|
@@ -60,11 +36,6 @@ function warnAboutDegradedRules(ctx: Context, config: Config): void {
|
|
|
60
36
|
}
|
|
61
37
|
}
|
|
62
38
|
|
|
63
|
-
/**
|
|
64
|
-
* 注册两个替换实现。
|
|
65
|
-
* @param ctx - 插件上下文(官方 `sandbox` / `fs-sandbox` 行已禁用)。
|
|
66
|
-
* @param config - 已由 schema 填好默认值的配置。
|
|
67
|
-
*/
|
|
68
39
|
export function apply(ctx: Context, config: Config): void {
|
|
69
40
|
warnAboutDegradedRules(ctx, config);
|
|
70
41
|
new ConfigurableSandboxProvider(ctx, config);
|
package/src/rules.ts
CHANGED
|
@@ -1,32 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 规则编译:`access` 条目解析(`rw <path>` 额外可写根 / `r- <path>` 只读 /
|
|
3
|
-
* `-- <pattern>` 拒绝访问)、`{{ env.NAME }}` 模板展开、相对工作区路径的绝对化、
|
|
4
|
-
* glob → 正则,以及编译结果的匹配。
|
|
5
|
-
*
|
|
6
|
-
* 命中优先级:`--` 拒绝覆盖一切;`r-` 只读覆盖可写授予;都未命中时才按可写根判定。
|
|
7
|
-
*
|
|
8
|
-
* 生成的正则源码同时交给进程内检查(JS `RegExp`)与 macOS Seatbelt profile
|
|
9
|
-
* (SBPL 的 `(regex #"…")`),因此只用 POSIX ERE 与 JS 正则共有的语法。
|
|
10
|
-
* @module @morlay/dsh-sandbox-local/rules
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
import { isAbsolute, resolve, sep } from "node:path";
|
|
14
2
|
import { canonicalPath, writableRoots } from "@deepseek-ai/dsh-sandbox";
|
|
15
3
|
import type { SandboxExecutionPolicy } from "@deepseek-ai/dsh-sandbox";
|
|
16
4
|
|
|
17
|
-
/** `{{ env.NAME }}` 模板;名字限定为环境变量的字符集。 */
|
|
18
5
|
const ENV_TEMPLATE = /\{\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
19
6
|
|
|
20
|
-
/** `access` 的条目形式:`rw <path>` / `r- <path>` / `-- <pattern>`;前缀后必须有空白。 */
|
|
21
7
|
const ACCESS_LINE = /^(rw|r-|--)(?:\s+(.*))?$/u;
|
|
22
8
|
|
|
23
|
-
/** glob 元字符:规则里出现即按模式匹配,否则按字面路径(含其全部后代)匹配。 */
|
|
24
9
|
const GLOB_META = /[*?[]/;
|
|
25
10
|
|
|
26
|
-
/** glob 翻译时需要转义的正则元字符(字符类内部除外)。 */
|
|
27
11
|
const REGEX_META = /[\\^$+.(){}|]/;
|
|
28
12
|
|
|
29
|
-
/** 展开规则里的 `{{ env.NAME }}`;引用未定义或为空的环境变量直接抛错。 */
|
|
30
13
|
export function expandEnvTemplates(value: string, env: NodeJS.ProcessEnv): string {
|
|
31
14
|
return value.replace(ENV_TEMPLATE, (_match, name: string) => {
|
|
32
15
|
const resolved = env[name];
|
|
@@ -39,13 +22,6 @@ export function expandEnvTemplates(value: string, env: NodeJS.ProcessEnv): strin
|
|
|
39
22
|
});
|
|
40
23
|
}
|
|
41
24
|
|
|
42
|
-
/**
|
|
43
|
-
* 把含 glob 的规则翻译成正则源码。
|
|
44
|
-
* `**` 跨目录层级(`**` 与“`**` 后接斜杠”都能匹配零层),`*` 与 `?` 不跨 `/`,
|
|
45
|
-
* 字符类透传(`[!…]` 按 glob 习惯翻成 `[^…]`),其余正则元字符转义。
|
|
46
|
-
* @param pattern - 已绝对化的 glob 规则。
|
|
47
|
-
* @returns 可同时被 JS `RegExp` 与 SBPL regex 接受的正则源码。
|
|
48
|
-
*/
|
|
49
25
|
export function globToRegexSource(pattern: string): string {
|
|
50
26
|
let source = "";
|
|
51
27
|
for (let index = 0; index < pattern.length; index += 1) {
|
|
@@ -89,39 +65,32 @@ export function globToRegexSource(pattern: string): string {
|
|
|
89
65
|
return source;
|
|
90
66
|
}
|
|
91
67
|
|
|
92
|
-
/** 规则来源:模板已在加载期展开,路径仍是相对/绝对的原始拼写。 */
|
|
93
68
|
export interface RuleSource {
|
|
94
|
-
/** `rw` 条目:额外可写根。 */
|
|
95
69
|
readonly allowWrite: readonly string[];
|
|
96
|
-
|
|
70
|
+
|
|
97
71
|
readonly readOnly: readonly string[];
|
|
98
|
-
|
|
72
|
+
|
|
99
73
|
readonly deny: readonly string[];
|
|
100
74
|
}
|
|
101
75
|
|
|
102
|
-
/** 一条已编译的模式:源码同时供 JS `RegExp` 与 SBPL regex 使用。 */
|
|
103
76
|
export interface CompiledPattern {
|
|
104
|
-
/** 正则源码。 */
|
|
105
77
|
readonly source: string;
|
|
106
|
-
|
|
78
|
+
|
|
107
79
|
readonly regex: RegExp;
|
|
108
80
|
}
|
|
109
81
|
|
|
110
|
-
/** 编译后的规则:按一个工作区根绝对化后的可写根、只读项与拒绝项。 */
|
|
111
82
|
export interface CompiledRules {
|
|
112
|
-
/** 额外可写根(canonical 绝对路径)。 */
|
|
113
83
|
readonly allowRoots: readonly string[];
|
|
114
|
-
|
|
84
|
+
|
|
115
85
|
readonly readOnlySubtrees: readonly string[];
|
|
116
|
-
|
|
86
|
+
|
|
117
87
|
readonly readOnlyPatterns: readonly CompiledPattern[];
|
|
118
|
-
|
|
88
|
+
|
|
119
89
|
readonly denySubtrees: readonly string[];
|
|
120
|
-
|
|
90
|
+
|
|
121
91
|
readonly denyPatterns: readonly CompiledPattern[];
|
|
122
92
|
}
|
|
123
93
|
|
|
124
|
-
/** 规则是否为空——空规则下 provider 不改写任何 runner 参数。 */
|
|
125
94
|
export function isEmptyRules(rules: CompiledRules): boolean {
|
|
126
95
|
return (
|
|
127
96
|
rules.allowRoots.length === 0 &&
|
|
@@ -132,18 +101,10 @@ export function isEmptyRules(rules: CompiledRules): boolean {
|
|
|
132
101
|
);
|
|
133
102
|
}
|
|
134
103
|
|
|
135
|
-
/** 只读模式不因 `rw` 条目放松:投影出只保留 `r-` / `--` 条目的规则集。 */
|
|
136
104
|
export function withoutAllowRoots(rules: CompiledRules): CompiledRules {
|
|
137
105
|
return rules.allowRoots.length === 0 ? rules : { ...rules, allowRoots: [] };
|
|
138
106
|
}
|
|
139
107
|
|
|
140
|
-
/**
|
|
141
|
-
* 解析 `access` 配置:接受字符串数组(每项一条规则)或多行文本(每行一条规则),
|
|
142
|
-
* 空行忽略;`rw <path>` 是额外可写根,`r- <path>` 是只读,`-- <pattern>` 是访问
|
|
143
|
-
* 拒绝(读 + 写)。缺前缀或前缀后没有路径都直接报错——规则不因写法歧义而变形。
|
|
144
|
-
* @param input - 配置里的 `access` 值。
|
|
145
|
-
* @returns `allowWrite` / `readOnly` / `deny` 三组规则(模板尚未展开)。
|
|
146
|
-
*/
|
|
147
108
|
export function parseAccess(input: string | readonly string[] | undefined): RuleSource {
|
|
148
109
|
const lines = (input === undefined ? [] : typeof input === "string" ? [input] : [...input])
|
|
149
110
|
.flatMap((value) => value.split(/\r?\n/u))
|
|
@@ -170,13 +131,6 @@ export function parseAccess(input: string | readonly string[] | undefined): Rule
|
|
|
170
131
|
return { allowWrite, readOnly, deny };
|
|
171
132
|
}
|
|
172
133
|
|
|
173
|
-
/**
|
|
174
|
-
* 把配置里的 `access` 转成规则来源:模板在这一步展开(加载期,fail-fast),
|
|
175
|
-
* 相对路径留待按调用时的工作区根绝对化。
|
|
176
|
-
* @param config - 含 `access` 的插件配置。
|
|
177
|
-
* @param env - 模板展开用的进程环境。
|
|
178
|
-
* @returns 模板已展开的规则来源。
|
|
179
|
-
*/
|
|
180
134
|
export function ruleSourceOf(
|
|
181
135
|
config: { access?: string | readonly string[] },
|
|
182
136
|
env: NodeJS.ProcessEnv,
|
|
@@ -191,12 +145,10 @@ export function ruleSourceOf(
|
|
|
191
145
|
};
|
|
192
146
|
}
|
|
193
147
|
|
|
194
|
-
/** 绝对化一条规则:相对路径相对工作区根。 */
|
|
195
148
|
function absolutize(value: string, workspaceRoot: string): string {
|
|
196
149
|
return isAbsolute(value) ? value : resolve(workspaceRoot, value);
|
|
197
150
|
}
|
|
198
151
|
|
|
199
|
-
/** 编译一组字面路径 / glob 规则:字面项按子树(含全部后代)匹配,glob 项按整串匹配。 */
|
|
200
152
|
function compilePaths(
|
|
201
153
|
values: readonly string[],
|
|
202
154
|
workspaceRoot: string,
|
|
@@ -215,13 +167,6 @@ function compilePaths(
|
|
|
215
167
|
return { subtrees, patterns };
|
|
216
168
|
}
|
|
217
169
|
|
|
218
|
-
/**
|
|
219
|
-
* 按一个工作区根编译规则。
|
|
220
|
-
* `rw` 条目必须是具体路径(glob 无法表达“可写根”);`r-` 与 `--` 条目允许 glob。
|
|
221
|
-
* @param source - 已展开模板的规则来源。
|
|
222
|
-
* @param workspaceRoot - 相对规则解析用的工作区根(canonical 绝对路径)。
|
|
223
|
-
* @returns 编译后的规则。
|
|
224
|
-
*/
|
|
225
170
|
export function compileRules(source: RuleSource, workspaceRoot: string): CompiledRules {
|
|
226
171
|
const allowRoots: string[] = [];
|
|
227
172
|
for (const value of source.allowWrite) {
|
|
@@ -241,13 +186,6 @@ export function compileRules(source: RuleSource, workspaceRoot: string): Compile
|
|
|
241
186
|
};
|
|
242
187
|
}
|
|
243
188
|
|
|
244
|
-
/**
|
|
245
|
-
* 一次调用可写入的根集合:官方 `writableRoots` 加上 `rw` 条目;`read-only` 不追加
|
|
246
|
-
* (额外可写根不放松显式选定的只读边界)。
|
|
247
|
-
* @param rules - 已编译规则。
|
|
248
|
-
* @param policy - per-call 策略。
|
|
249
|
-
* @returns canonical 可写根列表。
|
|
250
|
-
*/
|
|
251
189
|
export function writableRootsWith(
|
|
252
190
|
rules: CompiledRules,
|
|
253
191
|
policy: SandboxExecutionPolicy,
|
|
@@ -256,13 +194,11 @@ export function writableRootsWith(
|
|
|
256
194
|
return policy.mode === "workspace-write" ? [...roots, ...rules.allowRoots] : roots;
|
|
257
195
|
}
|
|
258
196
|
|
|
259
|
-
/** 字面规则的后代判定(canonical 拼写,与 Seatbelt `subpath` 同一语义)。 */
|
|
260
197
|
function isUnder(path: string, root: string): boolean {
|
|
261
198
|
if (path === root) return true;
|
|
262
199
|
return path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
|
|
263
200
|
}
|
|
264
201
|
|
|
265
|
-
/** 命中任一子树或任一模式。 */
|
|
266
202
|
function matches(
|
|
267
203
|
canonicalTarget: string,
|
|
268
204
|
subtrees: readonly string[],
|
|
@@ -274,32 +210,14 @@ function matches(
|
|
|
274
210
|
return patterns.some((pattern) => pattern.regex.test(canonicalTarget));
|
|
275
211
|
}
|
|
276
212
|
|
|
277
|
-
/**
|
|
278
|
-
* 判断目标是否被 `--` 条目命中(读与写都拒)。
|
|
279
|
-
* @param rules - 已编译规则。
|
|
280
|
-
* @param canonicalTarget - 目标的 canonical 路径。
|
|
281
|
-
* @returns 命中即 true。
|
|
282
|
-
*/
|
|
283
213
|
export function isDenied(rules: CompiledRules, canonicalTarget: string): boolean {
|
|
284
214
|
return matches(canonicalTarget, rules.denySubtrees, rules.denyPatterns);
|
|
285
215
|
}
|
|
286
216
|
|
|
287
|
-
/**
|
|
288
|
-
* 判断目标是否被 `r-` 条目命中(读放行、写拒绝)。
|
|
289
|
-
* @param rules - 已编译规则。
|
|
290
|
-
* @param canonicalTarget - 目标的 canonical 路径。
|
|
291
|
-
* @returns 命中即 true。
|
|
292
|
-
*/
|
|
293
217
|
export function isReadOnly(rules: CompiledRules, canonicalTarget: string): boolean {
|
|
294
218
|
return matches(canonicalTarget, rules.readOnlySubtrees, rules.readOnlyPatterns);
|
|
295
219
|
}
|
|
296
220
|
|
|
297
|
-
/**
|
|
298
|
-
* 判断目标是否不可写:`--` 与 `r-` 条目都拒绝写入,且优先于任何可写根。
|
|
299
|
-
* @param rules - 已编译规则。
|
|
300
|
-
* @param canonicalTarget - 目标的 canonical 路径。
|
|
301
|
-
* @returns 命中即 true。
|
|
302
|
-
*/
|
|
303
221
|
export function blocksWrite(rules: CompiledRules, canonicalTarget: string): boolean {
|
|
304
222
|
return isDenied(rules, canonicalTarget) || isReadOnly(rules, canonicalTarget);
|
|
305
223
|
}
|
package/src/sandbox.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 进程沙箱 provider:继承官方 `@deepseek-ai/dsh-sandbox-local` 的实现(runner 探测与
|
|
3
|
-
* 选择、Windows ACL 的私有 temp 流程、拒绝方言与 runner 失败规则全部保留),
|
|
4
|
-
* 只在 `confine` 产出的 argv 上追加本包的 `access` 条目(`rw` / `r-` / `--`)。
|
|
5
|
-
*
|
|
6
|
-
* 服务键沿用上游基类的 `ctx.sandbox`;装配时官方 `sandbox` 行必须被禁用,
|
|
7
|
-
* 否则同名服务会 fail loud。
|
|
8
|
-
* @module @morlay/dsh-sandbox-local/sandbox
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
12
2
|
import type { ConfinedArgv, SandboxPolicy } from "@deepseek-ai/dsh-sandbox";
|
|
13
3
|
import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
|
|
@@ -22,14 +12,9 @@ import {
|
|
|
22
12
|
type RuleSource,
|
|
23
13
|
} from "./rules.ts";
|
|
24
14
|
|
|
25
|
-
/**
|
|
26
|
-
* 官方本机沙箱 provider 的可配置版本。
|
|
27
|
-
* `read-only` 模式不追加 `rw` 条目(只读边界不因额外可写根放松);`--` 条目在两种
|
|
28
|
-
* confined 模式下都生效。
|
|
29
|
-
*/
|
|
30
15
|
export class ConfigurableSandboxProvider extends LocalSandboxProvider {
|
|
31
16
|
private readonly source: RuleSource;
|
|
32
|
-
|
|
17
|
+
|
|
33
18
|
private readonly compiled = new Map<string, CompiledRules>();
|
|
34
19
|
|
|
35
20
|
constructor(ctx: Context, config: Config) {
|
|
@@ -37,21 +22,18 @@ export class ConfigurableSandboxProvider extends LocalSandboxProvider {
|
|
|
37
22
|
this.source = ruleSourceOf(config, process.env);
|
|
38
23
|
}
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
override confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv {
|
|
47
|
-
const confined = super.confine(argv, policy);
|
|
25
|
+
override async confine(
|
|
26
|
+
argv: readonly string[],
|
|
27
|
+
policy: SandboxPolicy,
|
|
28
|
+
signal?: AbortSignal,
|
|
29
|
+
): Promise<ConfinedArgv> {
|
|
30
|
+
const confined = await super.confine(argv, policy, signal);
|
|
48
31
|
const rules = this.rulesFor(policy.workspaceRoot);
|
|
49
32
|
const effective = policy.mode === "workspace-write" ? rules : withoutAllowRoots(rules);
|
|
50
33
|
if (isEmptyRules(effective)) return confined;
|
|
51
34
|
return { ...confined, argv: extendConfinedArgv(confined.argv, effective) };
|
|
52
35
|
}
|
|
53
36
|
|
|
54
|
-
/** 取(并按需编译缓存)某个工作区根下的规则。 */
|
|
55
37
|
private rulesFor(workspaceRoot: string): CompiledRules {
|
|
56
38
|
const cached = this.compiled.get(workspaceRoot);
|
|
57
39
|
if (cached !== undefined) return cached;
|