@morlay/dsh-sandbox-local 0.0.3-alpha.4 → 0.0.3-alpha.6

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 CHANGED
@@ -97,7 +97,7 @@
97
97
  `sandbox-local` 行)与 `access` 规则一起维护在 preset 的 bundle patch 里,因此示例 app 的
98
98
  `dsh.profile.bundles` 不需要列出本包,只需要 profile 的依赖树能解析模块名
99
99
  (`@morlay/dsh-preset` 已在 `dependencies` 声明本包)。patch 层级的合并顺序与放置理由见
100
- [设计 预设生成与装配](../../preset/dsh-preset/.agents/designs/20260917-预设生成与装配.md)。
100
+ [设计 host 层部署配置](../../preset/dsh-preset/.agents/designs/20260917-host层部署配置.md)。
101
101
  两种采用方式互斥:同时上线会重复插入同一行。
102
102
 
103
103
  ## 前提
package/dist/index.mjs CHANGED
@@ -1,10 +1,48 @@
1
- import { dirname, isAbsolute, resolve, sep } from "node:path";
2
- import { canonicalPath, writableRoots } from "@deepseek-ai/dsh-sandbox";
3
1
  import { FsError } from "@deepseek-ai/dsh-fs";
4
2
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
5
3
  import { stat } from "node:fs/promises";
4
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
5
+ import { canonicalPath, writableRoots } from "@deepseek-ai/dsh-sandbox";
6
6
  import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
7
7
  import z from "@deepseek-ai/schemastery";
8
+ //#region src/containment.ts
9
+ const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
10
+ async function statIfPresent(path) {
11
+ try {
12
+ return await stat(path, { bigint: true });
13
+ } catch (error) {
14
+ const code = error.code;
15
+ if (code !== void 0 && MISSING_CODES.has(code)) return void 0;
16
+ throw error;
17
+ }
18
+ }
19
+ function comparablePath(path, caseSensitive) {
20
+ return caseSensitive ? path : path.toLowerCase();
21
+ }
22
+ function isLexicallyUnder(path, root, caseSensitive) {
23
+ const comparableTarget = comparablePath(path, caseSensitive);
24
+ const comparableRoot = comparablePath(root, caseSensitive);
25
+ if (comparableTarget === comparableRoot) return true;
26
+ const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
27
+ return comparableTarget.startsWith(prefix);
28
+ }
29
+ function sameIdentity(left, right) {
30
+ return left.dev === right.dev && left.ino === right.ino;
31
+ }
32
+ async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
33
+ if (isLexicallyUnder(path, root, caseSensitive)) return true;
34
+ const rootInfo = await statIfPresent(root);
35
+ if (rootInfo === void 0) return false;
36
+ let ancestor = path;
37
+ for (;;) {
38
+ const ancestorInfo = await statIfPresent(ancestor);
39
+ if (ancestorInfo !== void 0 && sameIdentity(ancestorInfo, rootInfo)) return true;
40
+ const parent = dirname(ancestor);
41
+ if (parent === ancestor) return false;
42
+ ancestor = parent;
43
+ }
44
+ }
45
+ //#endregion
8
46
  //#region src/rules.ts
9
47
  const ENV_TEMPLATE = /\{\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
10
48
  const ACCESS_LINE = /^(rw|r-|--)(?:\s+(.*))?$/u;
@@ -130,6 +168,10 @@ function compileRules(source, workspaceRoot) {
130
168
  denyPatterns: deny.patterns
131
169
  };
132
170
  }
171
+ function writableRootsWith(rules, policy) {
172
+ const roots = writableRoots(policy);
173
+ return policy.mode === "workspace-write" ? [...roots, ...rules.allowRoots] : roots;
174
+ }
133
175
  function isUnder(path, root) {
134
176
  if (path === root) return true;
135
177
  return path.startsWith(root.endsWith(sep) ? root : `${root}${sep}`);
@@ -145,34 +187,63 @@ function isReadOnly(rules, canonicalTarget) {
145
187
  return matches(canonicalTarget, rules.readOnlySubtrees, rules.readOnlyPatterns);
146
188
  }
147
189
  //#endregion
148
- //#region src/dialects.ts
149
- const SEPARATOR = "--";
150
- const DIALECT_CAPABILITIES = {
151
- seatbelt: {
152
- allowWrite: true,
153
- readOnly: true,
154
- denyReadWrite: true,
155
- denyWriteOnly: false
156
- },
157
- bwrap: {
158
- allowWrite: true,
159
- readOnly: true,
160
- denyReadWrite: false,
161
- denyWriteOnly: true
162
- },
163
- landlock: {
164
- allowWrite: true,
165
- readOnly: false,
166
- denyReadWrite: false,
167
- denyWriteOnly: false
168
- },
169
- "windows-acl": {
170
- allowWrite: false,
171
- readOnly: false,
172
- denyReadWrite: false,
173
- denyWriteOnly: false
190
+ //#region src/fs.ts
191
+ var ConfigurableFileSystem = class extends LocalFileSystem {
192
+ static inject = ["sandboxPolicy"];
193
+ defaultMode;
194
+ source;
195
+ compiled = /* @__PURE__ */ new Map();
196
+ constructor(ctx, config) {
197
+ super(ctx, config);
198
+ this.defaultMode = ctx.sandboxPolicy.defaultMode;
199
+ this.source = ruleSourceOf(config, process.env);
200
+ }
201
+ get sandboxMode() {
202
+ return this.defaultMode;
203
+ }
204
+ async resolve(path, opts) {
205
+ const target = await super.resolve(path, opts);
206
+ this.assertNotDenied(this.rulesFor(this.ctx.sandboxPolicy.resolve().workspaceRoot), target.targetKey, target.displayPath);
207
+ return target;
208
+ }
209
+ async writeText(target, content, expected, signal, sandboxPolicy) {
210
+ return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
211
+ }
212
+ async editText(target, edit, expected, signal, sandboxPolicy) {
213
+ return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
214
+ }
215
+ assertNotDenied(rules, canonicalTarget, displayPath) {
216
+ if (!isDenied(rules, canonicalTarget)) return;
217
+ throw new FsError(`cannot access "${displayPath}": file access denied by the configured "--" rules`, "FS_SANDBOX_DENIED");
218
+ }
219
+ assertWritable(rules, canonicalTarget, displayPath) {
220
+ this.assertNotDenied(rules, canonicalTarget, displayPath);
221
+ if (!isReadOnly(rules, canonicalTarget)) return;
222
+ throw new FsError(`cannot write "${displayPath}": path is read-only by the configured "r-" rule`, "FS_SANDBOX_DENIED");
223
+ }
224
+ async checkedTarget(target, sandboxPolicy) {
225
+ const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
226
+ const rules = this.rulesFor(policy.workspaceRoot);
227
+ this.assertWritable(rules, target.targetKey, target.displayPath);
228
+ const { mode } = policy;
229
+ if (mode === "danger-full-access") return target;
230
+ if (mode === "read-only") throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
231
+ const fresh = await super.resolve(target.displayPath);
232
+ this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
233
+ for (const root of writableRootsWith(rules, policy)) if (await isPathUnder(fresh.targetKey, root)) return fresh;
234
+ throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
235
+ }
236
+ rulesFor(workspaceRoot) {
237
+ const cached = this.compiled.get(workspaceRoot);
238
+ if (cached !== void 0) return cached;
239
+ const compiled = compileRules(this.source, workspaceRoot);
240
+ this.compiled.set(workspaceRoot, compiled);
241
+ return compiled;
174
242
  }
175
243
  };
244
+ //#endregion
245
+ //#region src/dialects.ts
246
+ const SEPARATOR = "--";
176
247
  function separatorIndex(argv) {
177
248
  const index = argv.indexOf(SEPARATOR);
178
249
  if (index === -1) throw new Error("sandbox rules: the confined argv carries no `--` separator to extend");
@@ -242,99 +313,6 @@ function extendConfinedArgv(argv, rules) {
242
313
  }
243
314
  }
244
315
  //#endregion
245
- //#region src/containment.ts
246
- const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
247
- async function statIfPresent(path) {
248
- try {
249
- return await stat(path, { bigint: true });
250
- } catch (error) {
251
- const code = error.code;
252
- if (code !== void 0 && MISSING_CODES.has(code)) return void 0;
253
- throw error;
254
- }
255
- }
256
- function comparablePath(path, caseSensitive) {
257
- return caseSensitive ? path : path.toLowerCase();
258
- }
259
- function isLexicallyUnder(path, root, caseSensitive) {
260
- const comparableTarget = comparablePath(path, caseSensitive);
261
- const comparableRoot = comparablePath(root, caseSensitive);
262
- if (comparableTarget === comparableRoot) return true;
263
- const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
264
- return comparableTarget.startsWith(prefix);
265
- }
266
- function sameIdentity(left, right) {
267
- return left.dev === right.dev && left.ino === right.ino;
268
- }
269
- async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
270
- if (isLexicallyUnder(path, root, caseSensitive)) return true;
271
- const rootInfo = await statIfPresent(root);
272
- if (rootInfo === void 0) return false;
273
- let ancestor = path;
274
- for (;;) {
275
- const ancestorInfo = await statIfPresent(ancestor);
276
- if (ancestorInfo !== void 0 && sameIdentity(ancestorInfo, rootInfo)) return true;
277
- const parent = dirname(ancestor);
278
- if (parent === ancestor) return false;
279
- ancestor = parent;
280
- }
281
- }
282
- //#endregion
283
- //#region src/fs.ts
284
- var ConfigurableFileSystem = class extends LocalFileSystem {
285
- static inject = ["sandboxPolicy"];
286
- defaultMode;
287
- source;
288
- compiled = /* @__PURE__ */ new Map();
289
- constructor(ctx, config) {
290
- super(ctx, config);
291
- this.defaultMode = ctx.sandboxPolicy.defaultMode;
292
- this.source = ruleSourceOf(config, process.env);
293
- }
294
- get sandboxMode() {
295
- return this.defaultMode;
296
- }
297
- async resolve(path, opts) {
298
- const target = await super.resolve(path, opts);
299
- this.assertNotDenied(this.rulesFor(opts?.cwd ?? this.ctx.sandboxPolicy.workspaceRoot), target.targetKey, target.displayPath);
300
- return target;
301
- }
302
- async writeText(target, content, expected, signal, sandboxPolicy) {
303
- return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
304
- }
305
- async editText(target, edit, expected, signal, sandboxPolicy) {
306
- return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
307
- }
308
- assertNotDenied(rules, canonicalTarget, displayPath) {
309
- if (!isDenied(rules, canonicalTarget)) return;
310
- throw new FsError(`cannot access "${displayPath}": file access denied by the configured "--" rules`, "FS_SANDBOX_DENIED");
311
- }
312
- assertWritable(rules, canonicalTarget, displayPath) {
313
- this.assertNotDenied(rules, canonicalTarget, displayPath);
314
- if (!isReadOnly(rules, canonicalTarget)) return;
315
- throw new FsError(`cannot write "${displayPath}": path is read-only by the configured "r-" rule`, "FS_SANDBOX_DENIED");
316
- }
317
- async checkedTarget(target, sandboxPolicy) {
318
- const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
319
- const rules = this.rulesFor(policy.workspaceRoot);
320
- this.assertWritable(rules, target.targetKey, target.displayPath);
321
- const { mode } = policy;
322
- if (mode === "danger-full-access") return target;
323
- if (mode === "read-only") throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
324
- const fresh = await super.resolve(target.displayPath);
325
- this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
326
- for (const root of [...writableRoots(policy), ...rules.allowRoots]) if (await isPathUnder(fresh.targetKey, root)) return fresh;
327
- throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
328
- }
329
- rulesFor(workspaceRoot) {
330
- const cached = this.compiled.get(workspaceRoot);
331
- if (cached !== void 0) return cached;
332
- const compiled = compileRules(this.source, workspaceRoot);
333
- this.compiled.set(workspaceRoot, compiled);
334
- return compiled;
335
- }
336
- };
337
- //#endregion
338
316
  //#region src/sandbox.ts
339
317
  var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
340
318
  source;
@@ -382,11 +360,7 @@ function warnAboutDegradedRules(ctx, config) {
382
360
  const denials = rules.deny.length > 0;
383
361
  if (!grants && !readOnly && !denials) return;
384
362
  if (process.platform === "darwin") return;
385
- if (readOnly || denials) {
386
- const seatbelt = DIALECT_CAPABILITIES.seatbelt.denyReadWrite;
387
- const bwrap = DIALECT_CAPABILITIES.bwrap.denyWriteOnly;
388
- 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: ${bwrap}; Landlock and the Windows ACL runner cannot express a subpath rule at all: ${seatbelt} applies to Seatbelt only) — tools that read through ctx.fs stay covered`);
389
- }
363
+ 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`);
390
364
  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");
391
365
  }
392
366
  function apply(ctx, config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/dsh-sandbox-local",
3
- "version": "0.0.3-alpha.4",
3
+ "version": "0.0.3-alpha.6",
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",
@@ -27,23 +27,23 @@
27
27
  "./cordis.patch.yml": "./cordis.patch.yml"
28
28
  },
29
29
  "devDependencies": {
30
- "@deepseek-ai/cordis": "4.0.2",
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"
30
+ "@deepseek-ai/cordis": "4.0.3",
31
+ "@deepseek-ai/dsh-fs": "0.1.7-alpha.1",
32
+ "@deepseek-ai/dsh-fs-local": "0.1.7-alpha.1",
33
+ "@deepseek-ai/dsh-fs-sandbox": "0.1.7-alpha.1",
34
+ "@deepseek-ai/dsh-sandbox": "0.1.7-alpha.1",
35
+ "@deepseek-ai/dsh-sandbox-local": "0.1.7-alpha.1",
36
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.1"
37
37
  },
38
38
  "peerDependencies": {
39
- "@deepseek-ai/cordis": "4.0.2",
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
- "@deepseek-ai/schemastery": "3.18.2"
39
+ "@deepseek-ai/cordis": "4.0.3",
40
+ "@deepseek-ai/dsh-fs": "0.1.7-alpha.1",
41
+ "@deepseek-ai/dsh-fs-local": "0.1.7-alpha.1",
42
+ "@deepseek-ai/dsh-fs-sandbox": "0.1.7-alpha.1",
43
+ "@deepseek-ai/dsh-sandbox": "0.1.7-alpha.1",
44
+ "@deepseek-ai/dsh-sandbox-local": "0.1.7-alpha.1",
45
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.1",
46
+ "@deepseek-ai/schemastery": "3.18.3"
47
47
  },
48
48
  "dsh": {
49
49
  "bundle": {
package/src/fs.ts CHANGED
@@ -10,7 +10,6 @@ import type {
10
10
  } from "@deepseek-ai/dsh-fs";
11
11
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
12
12
  import type { Config } from "./config.ts";
13
- import { writableRoots } from "@deepseek-ai/dsh-sandbox";
14
13
  import type { SandboxExecutionPolicy, SandboxMode } from "@deepseek-ai/dsh-sandbox";
15
14
  import type {} from "@deepseek-ai/dsh-sandbox-policy";
16
15
  import { isPathUnder } from "./containment.ts";
@@ -19,6 +18,7 @@ import {
19
18
  isDenied,
20
19
  isReadOnly,
21
20
  ruleSourceOf,
21
+ writableRootsWith,
22
22
  type CompiledRules,
23
23
  type RuleSource,
24
24
  } from "./rules.ts";
@@ -46,8 +46,10 @@ export class ConfigurableFileSystem extends LocalFileSystem {
46
46
  opts?: { cwd?: string; signal?: AbortSignal },
47
47
  ): Promise<FsTarget> {
48
48
  const target = await super.resolve(path, opts);
49
+ // 规则里的相对路径相对**会话工作区**解析(`opts.cwd` 只是目标路径的解析基准,见包 README 的条目语法):
50
+ // 读写两侧同一个基准,读被拒的文件才不会在写路径上被放行。
49
51
  this.assertNotDenied(
50
- this.rulesFor(opts?.cwd ?? this.ctx.sandboxPolicy.workspaceRoot),
52
+ this.rulesFor(this.ctx.sandboxPolicy.resolve().workspaceRoot),
51
53
  target.targetKey,
52
54
  target.displayPath,
53
55
  );
@@ -118,7 +120,7 @@ export class ConfigurableFileSystem extends LocalFileSystem {
118
120
 
119
121
  const fresh = await super.resolve(target.displayPath);
120
122
  this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
121
- for (const root of [...writableRoots(policy), ...rules.allowRoots]) {
123
+ for (const root of writableRootsWith(rules, policy)) {
122
124
  if (await isPathUnder(fresh.targetKey, root)) return fresh;
123
125
  }
124
126
  throw new FsError(
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  import type { Config } from "./config.ts";
3
- import { DIALECT_CAPABILITIES } from "./dialects.ts";
4
3
  import { ConfigurableFileSystem } from "./fs.ts";
5
4
  import { ruleSourceOf } from "./rules.ts";
6
5
  import { ConfigurableSandboxProvider } from "./sandbox.ts";
@@ -19,12 +18,10 @@ function warnAboutDegradedRules(ctx: Context, config: Config): void {
19
18
  if (!grants && !readOnly && !denials) return;
20
19
  if (process.platform === "darwin") return;
21
20
  if (readOnly || denials) {
22
- const seatbelt = DIALECT_CAPABILITIES.seatbelt.denyReadWrite;
23
- const bwrap = DIALECT_CAPABILITIES.bwrap.denyWriteOnly;
24
21
  ctx.logger.warn(
25
22
  `sandbox-local: "r-" / "--" entries cannot be fully enforced for confined subprocesses on ${process.platform} ` +
26
- `(Seatbelt enforces both; bwrap binds the path read-only, so "--" degrades to write-only: ${bwrap}; ` +
27
- `Landlock and the Windows ACL runner cannot express a subpath rule at all: ${seatbelt} applies to Seatbelt only) ` +
23
+ '(Seatbelt enforces both; bwrap binds the path read-only, so "--" degrades to write-only; ' +
24
+ "Landlock and the Windows ACL runner cannot express a subpath rule at all) " +
28
25
  "— tools that read through ctx.fs stay covered",
29
26
  );
30
27
  }