@morlay/dsh-sandbox-local 0.0.3-alpha.8 → 0.0.3-alpha.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 CHANGED
@@ -43,6 +43,11 @@
43
43
 
44
44
  ## 配置
45
45
 
46
+ 六个字段都上设置页:`access` 可改(**改完规则当场重算**,不用重挂这一行);其余五个是**装配事实**,页面上只读
47
+ 可见——它们在 `Config` 上标了 `.volatile().disabled()`,所以能看清「这一行现在装配成什么样」而改不了。`volatile`
48
+ 会让解析后的值变成**引用**,而官方 `LocalSandboxProvider` / `LocalFileSystem` 在构造时读一次这些字段并做装配期
49
+ 校验,所以交给基类的那份配置先由 `upstreamConfigOf` 解包成值(`src/config.ts`)。
50
+
46
51
  | 字段 | 默认 | 含义 |
47
52
  | ------------------------- | --------------- | --------------------------------------------------------------------------------------------- |
48
53
  | `access` | `[]` | 规则条目:`rw <path>` 可写根 / `r- <path>` 只读 / `-- <pattern>` 拒绝访问;数组或一段多行文本 |
@@ -94,10 +99,10 @@
94
99
  (schema 默认是空规则),patch 内容见该文件。
95
100
 
96
101
  **本部署走的就是这条路径**:示例 app 的 `dsh.profile.bundles` 列出了本包(排在
97
- [`@morlay/dsh-profile`](../../profile/dsh-profile/README.md) 之前),因此"禁用官方两行 + 插入本行"由这份
102
+ [`@morlay/dsh-profile`](../../bundles/sandbox-profile/README.md) 之前),因此"禁用官方两行 + 插入本行"由这份
98
103
  patch 负责;`access` 规则的值由 `dsh-profile` 按 id 做 config 覆盖——装配与配置各归一处
99
104
  (`@morlay/dsh-profile` 已在 `dependencies` 声明本包)。patch 层级的合并顺序与放置理由见
100
- [设计 host 层部署配置](../../profile/dsh-profile/.agents/designs/20260917-host层部署配置.md)。
105
+ [设计 host 层部署配置](../../bundles/sandbox-profile/README.md)。
101
106
  两种采用方式互斥:同时上线会重复插入同一行。
102
107
 
103
108
  ## 接管 `sandbox:policy`
@@ -0,0 +1,55 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@morlay/dsh-sandbox-local",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region src/client/locales.ts
8
+ /** 本包在配置页上的字段文案(命名空间 `settings.sandbox-local`)。 */
9
+ const zh = {
10
+ access: "额外可写根与拒绝项",
11
+ accessHint: "一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;支持 `~`、环境变量与 glob。改完当场生效。"
12
+ };
13
+ const en = {
14
+ access: "Extra writable roots and refusals",
15
+ 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. Edits apply immediately."
16
+ };
17
+ //#endregion
18
+ //#region src/client/index.ts
19
+ /** 本包 host 行 id:行配置页读的就是这个命名空间。 */
20
+ const SANDBOX_NS = "sandbox-local";
21
+ /** 这一行里需要中文文案的字段。 */
22
+ const FIELDS = ["access"];
23
+ /** 本页字典的命名空间。 */
24
+ const NS = "settings.sandbox-local";
25
+ /** 需要的服务:槽位(字段槽)与字典。 */
26
+ const inject = ["locale"];
27
+ /**
28
+ * 给 `access` 补文案。
29
+ * @param ctx - 浏览器插件上下文。
30
+ */
31
+ function apply(ctx) {
32
+ const t = ctx.locale.bind(NS);
33
+ ctx.effect(() => ctx.locale.register(NS, {
34
+ zh,
35
+ en
36
+ }), "sandbox-local: field locale");
37
+ ctx.inject(["schemaFormHints"], (scope) => scope.effect(() => {
38
+ const hints = scope.schemaFormHints;
39
+ const offs = FIELDS.map((key) => hints.describe(SANDBOX_NS, [key], () => ({
40
+ label: t(key),
41
+ hint: t("accessHint")
42
+ })));
43
+ return () => {
44
+ for (const off of offs) off();
45
+ };
46
+ }, "sandbox-local: field wording"));
47
+ }
48
+ //#endregion
49
+ exports.NS = NS;
50
+ exports.SANDBOX_NS = SANDBOX_NS;
51
+ exports.apply = apply;
52
+ exports.inject = inject;
53
+ return module.exports;
54
+ }
55
+ });
@@ -0,0 +1,29 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/locales.d.ts
3
+ /** 本包在配置页上的字段文案(命名空间 `settings.sandbox-local`)。 */
4
+ declare const zh: {
5
+ readonly access: "额外可写根与拒绝项";
6
+ readonly accessHint: "一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;支持 `~`、环境变量与 glob。改完当场生效。";
7
+ };
8
+ type SandboxFieldLocaleKey = keyof typeof zh;
9
+ //#endregion
10
+ //#region src/client/index.d.ts
11
+ /** 本包 host 行 id:行配置页读的就是这个命名空间。 */
12
+ declare const SANDBOX_NS = "sandbox-local";
13
+ /** 本页字典的命名空间。 */
14
+ declare const NS = "settings.sandbox-local";
15
+ declare module "@deepseek-ai/dsh-client-ui-slots" {
16
+ interface LocaleNamespaceMap {
17
+ /** 设置页里这一行的字段文案。 */
18
+ "settings.sandbox-local": SandboxFieldLocaleKey;
19
+ }
20
+ }
21
+ /** 需要的服务:槽位(字段槽)与字典。 */
22
+ declare const inject: string[];
23
+ /**
24
+ * 给 `access` 补文案。
25
+ * @param ctx - 浏览器插件上下文。
26
+ */
27
+ declare function apply(ctx: Context): void;
28
+ //#endregion
29
+ export { NS, SANDBOX_NS, apply, inject };
@@ -0,0 +1,29 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/locales.d.ts
3
+ /** 本包在配置页上的字段文案(命名空间 `settings.sandbox-local`)。 */
4
+ declare const zh: {
5
+ readonly access: "额外可写根与拒绝项";
6
+ readonly accessHint: "一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;支持 `~`、环境变量与 glob。改完当场生效。";
7
+ };
8
+ type SandboxFieldLocaleKey = keyof typeof zh;
9
+ //#endregion
10
+ //#region src/client/index.d.ts
11
+ /** 本包 host 行 id:行配置页读的就是这个命名空间。 */
12
+ declare const SANDBOX_NS = "sandbox-local";
13
+ /** 本页字典的命名空间。 */
14
+ declare const NS = "settings.sandbox-local";
15
+ declare module "@deepseek-ai/dsh-client-ui-slots" {
16
+ interface LocaleNamespaceMap {
17
+ /** 设置页里这一行的字段文案。 */
18
+ "settings.sandbox-local": SandboxFieldLocaleKey;
19
+ }
20
+ }
21
+ /** 需要的服务:槽位(字段槽)与字典。 */
22
+ declare const inject: string[];
23
+ /**
24
+ * 给 `access` 补文案。
25
+ * @param ctx - 浏览器插件上下文。
26
+ */
27
+ declare function apply(ctx: Context): void;
28
+ //#endregion
29
+ export { NS, SANDBOX_NS, apply, inject };
@@ -0,0 +1,32 @@
1
+ import { Context } from "@deepseek-ai/cordis";
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
+ //#region src/config.d.ts
7
+ interface Config extends Config$2, Config$1 {
8
+ access?: string | string[];
9
+ }
10
+ /**
11
+ * schema 解析之后的形状:`access` 是 **volatile 稳定引用**(页面可改的那一项),读它要过 `.get()`。
12
+ *
13
+ * 其余字段也是 volatile,但页面把它们画成**只读**(schema 上的 `disabled()`):它们是装配事实——runner 命令、
14
+ * 进程 cwd、上游基类读一次的超时与差额上限——放在页面上是为了看清「这一行现在装配成什么样」,改它们要重挂
15
+ * 这一行。所以给上游基类的那份配置是**解包后的值**({@link upstreamConfigOf}):基类读的是值,不是引用。
16
+ */
17
+ interface ResolvedConfig extends Omit<Config, "access" | "runnerCommand" | "runnerFailureSignatures" | "probeTimeoutMs" | "cwd" | "diffBasisMaxBytes"> {
18
+ readonly access: Volatile<string | string[]>;
19
+ readonly runnerCommand: Volatile<readonly string[]>;
20
+ readonly runnerFailureSignatures: Volatile<readonly string[]>;
21
+ readonly probeTimeoutMs: Volatile<number>;
22
+ readonly cwd: Volatile<string>;
23
+ readonly diffBasisMaxBytes: Volatile<number>;
24
+ }
25
+ declare const Config: z<Config, ResolvedConfig>;
26
+ //#endregion
27
+ //#region src/index.d.ts
28
+ declare const name = "sandbox-local";
29
+ declare const inject: string[];
30
+ declare function apply(ctx: Context, config: ResolvedConfig): void;
31
+ //#endregion
32
+ export { Config, type ResolvedConfig, apply, inject, name };
package/dist/index.d.mts CHANGED
@@ -1,16 +1,32 @@
1
1
  import { Config as Config$1 } from "@deepseek-ai/dsh-fs-local";
2
- import { Config as Config$2 } from "@deepseek-ai/dsh-sandbox-local";
3
2
  import z from "@deepseek-ai/schemastery";
3
+ import { Config as Config$2 } from "@deepseek-ai/dsh-sandbox-local";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
+ import { Volatile } from "@deepseek-ai/cosmokit";
5
6
  //#region src/config.d.ts
6
7
  interface Config extends Config$2, Config$1 {
7
8
  access?: string | string[];
8
9
  }
9
- declare const Config: z<Config>;
10
+ /**
11
+ * schema 解析之后的形状:`access` 是 **volatile 稳定引用**(页面可改的那一项),读它要过 `.get()`。
12
+ *
13
+ * 其余字段也是 volatile,但页面把它们画成**只读**(schema 上的 `disabled()`):它们是装配事实——runner 命令、
14
+ * 进程 cwd、上游基类读一次的超时与差额上限——放在页面上是为了看清「这一行现在装配成什么样」,改它们要重挂
15
+ * 这一行。所以给上游基类的那份配置是**解包后的值**({@link upstreamConfigOf}):基类读的是值,不是引用。
16
+ */
17
+ interface ResolvedConfig extends Omit<Config, "access" | "runnerCommand" | "runnerFailureSignatures" | "probeTimeoutMs" | "cwd" | "diffBasisMaxBytes"> {
18
+ readonly access: Volatile<string | string[]>;
19
+ readonly runnerCommand: Volatile<readonly string[]>;
20
+ readonly runnerFailureSignatures: Volatile<readonly string[]>;
21
+ readonly probeTimeoutMs: Volatile<number>;
22
+ readonly cwd: Volatile<string>;
23
+ readonly diffBasisMaxBytes: Volatile<number>;
24
+ }
25
+ declare const Config: z<Config, ResolvedConfig>;
10
26
  //#endregion
11
27
  //#region src/index.d.ts
12
28
  declare const name = "sandbox-local";
13
29
  declare const inject: string[];
14
- declare function apply(ctx: Context, config: Config): void;
30
+ declare function apply(ctx: Context, config: ResolvedConfig): void;
15
31
  //#endregion
16
- export { Config, apply, inject, name };
32
+ export { Config, type ResolvedConfig, apply, inject, name };
package/dist/index.mjs CHANGED
@@ -1,10 +1,60 @@
1
1
  import { FsError } from "@deepseek-ai/dsh-fs";
2
2
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
3
+ import z from "@deepseek-ai/schemastery";
3
4
  import { stat } from "node:fs/promises";
4
5
  import { dirname, isAbsolute, resolve, sep } from "node:path";
5
6
  import { canonicalPath, writableRoots } from "@deepseek-ai/dsh-sandbox";
6
7
  import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
7
- import z from "@deepseek-ai/schemastery";
8
+ //#region src/config.ts
9
+ /**
10
+ * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
11
+ * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
12
+ */
13
+ const localized = (text) => text;
14
+ const Config = z.object({
15
+ access: z.union([z.array(z.string()), z.string()]).default([]).description(localized({
16
+ zh: "额外可写根与拒绝项,一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;支持 `~`、环境变量与 glob。改它当场生效(沙箱规则每次按当前值重算),不用重挂这一行。",
17
+ 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 are supported. Edits take effect on the next rule compile without remounting the row."
18
+ })).volatile(),
19
+ runnerCommand: z.array(z.string()).default([]).disabled().description(localized({
20
+ zh: "沙箱 runner 的启动命令;空数组表示按平台自带的链去选。改它要重挂这一行。",
21
+ en: "Runner command for confined subprocesses; an empty array picks the platform chain. Edits apply on remount."
22
+ })).volatile(),
23
+ runnerFailureSignatures: z.array(z.string()).default([]).disabled().description(localized({
24
+ zh: "runner 命令「用不了」的判定片段,与 `runnerCommand` 成对出现。",
25
+ en: "Fragments that mark the runner command unusable; declared together with `runnerCommand`."
26
+ })).volatile(),
27
+ probeTimeoutMs: z.natural().default(5e3).disabled().description(localized({
28
+ zh: "探测 runner 可用性的超时(毫秒)。",
29
+ en: "Timeout in ms for probing whether the runner works."
30
+ })).volatile(),
31
+ cwd: z.string().default(process.cwd()).disabled().description(localized({
32
+ zh: "相对路径的基准目录;默认是启动这个进程时的工作目录。",
33
+ en: "Base directory for relative paths; defaults to the process working directory."
34
+ })).volatile(),
35
+ diffBasisMaxBytes: z.number().default(10485760).disabled().description(localized({
36
+ zh: "覆盖写差异的单侧字节上限(默认 10 MiB)。",
37
+ en: "Per-side byte cap for overwrite diffs (10 MiB by default)."
38
+ })).volatile()
39
+ });
40
+ /**
41
+ * 交给上游基类的那份配置:装配事实取当前值。
42
+ *
43
+ * 上游 `LocalSandboxProvider` / `LocalFileSystem` 在构造时读一次这些字段并做装配期校验,所以它们要的是值;
44
+ * 设置页改它们走的是「写盘 → Loader 重挂这一行 → 重新解析」这条路。
45
+ * @param config - 本行解析后的配置。
46
+ * @returns 上游两个基类认的普通配置对象。
47
+ */
48
+ function upstreamConfigOf(config) {
49
+ return {
50
+ runnerCommand: [...config.runnerCommand.get()],
51
+ runnerFailureSignatures: [...config.runnerFailureSignatures.get()],
52
+ probeTimeoutMs: config.probeTimeoutMs.get(),
53
+ cwd: config.cwd.get(),
54
+ diffBasisMaxBytes: config.diffBasisMaxBytes.get()
55
+ };
56
+ }
57
+ //#endregion
8
58
  //#region src/containment.ts
9
59
  const MISSING_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
10
60
  async function statIfPresent(path) {
@@ -122,8 +172,8 @@ function parseAccess(input) {
122
172
  deny
123
173
  };
124
174
  }
125
- function ruleSourceOf(config, env) {
126
- const parsed = parseAccess(config.access);
175
+ function ruleSourceOf(access, env) {
176
+ const parsed = parseAccess(access);
127
177
  const expand = (values) => values.map((value) => expandEnvTemplates(value, env));
128
178
  return {
129
179
  allowWrite: expand(parsed.allowWrite),
@@ -191,12 +241,16 @@ function isReadOnly(rules, canonicalTarget) {
191
241
  var ConfigurableFileSystem = class extends LocalFileSystem {
192
242
  static inject = ["sandboxPolicy"];
193
243
  defaultMode;
194
- source;
244
+ access;
195
245
  compiled = /* @__PURE__ */ new Map();
196
246
  constructor(ctx, config) {
197
- super(ctx, config);
247
+ super(ctx, upstreamConfigOf(config));
198
248
  this.defaultMode = ctx.sandboxPolicy.defaultMode;
199
- this.source = ruleSourceOf(config, process.env);
249
+ this.access = config.access;
250
+ }
251
+ /** 规则源现场重算:`access` 是 volatile 引用,页面改完下一次编译就是新规则。 */
252
+ get source() {
253
+ return ruleSourceOf(this.access.get(), process.env);
200
254
  }
201
255
  get sandboxMode() {
202
256
  return this.defaultMode;
@@ -233,11 +287,14 @@ var ConfigurableFileSystem = class extends LocalFileSystem {
233
287
  for (const root of writableRootsWith(rules, policy)) if (await isPathUnder(fresh.targetKey, root)) return fresh;
234
288
  throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
235
289
  }
290
+ /** 编译结果按「工作区 + 当前 access」缓存:access 是页面可改的引用,改了就是另一份规则。 */
236
291
  rulesFor(workspaceRoot) {
237
- const cached = this.compiled.get(workspaceRoot);
292
+ const access = this.access.get();
293
+ const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
294
+ const cached = this.compiled.get(key);
238
295
  if (cached !== void 0) return cached;
239
- const compiled = compileRules(this.source, workspaceRoot);
240
- this.compiled.set(workspaceRoot, compiled);
296
+ const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
297
+ this.compiled.set(key, compiled);
241
298
  return compiled;
242
299
  }
243
300
  };
@@ -385,11 +442,15 @@ function extendConfinedArgv(argv, rules) {
385
442
  //#endregion
386
443
  //#region src/sandbox.ts
387
444
  var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
388
- source;
445
+ access;
389
446
  compiled = /* @__PURE__ */ new Map();
390
447
  constructor(ctx, config) {
391
- super(ctx, config);
392
- this.source = ruleSourceOf(config, process.env);
448
+ super(ctx, upstreamConfigOf(config));
449
+ this.access = config.access;
450
+ }
451
+ /** 规则源现场重算:`access` 是 volatile 引用,页面改完下一次编译就是新规则。 */
452
+ get source() {
453
+ return ruleSourceOf(this.access.get(), process.env);
393
454
  }
394
455
  async confine(argv, policy, signal) {
395
456
  const confined = await super.confine(argv, policy, signal);
@@ -401,25 +462,18 @@ var ConfigurableSandboxProvider = class extends LocalSandboxProvider {
401
462
  argv: extendConfinedArgv(confined.argv, effective)
402
463
  };
403
464
  }
465
+ /** 编译结果按「工作区 + 当前 access」缓存:access 是页面可改的引用,改了就是另一份规则。 */
404
466
  rulesFor(workspaceRoot) {
405
- const cached = this.compiled.get(workspaceRoot);
467
+ const access = this.access.get();
468
+ const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
469
+ const cached = this.compiled.get(key);
406
470
  if (cached !== void 0) return cached;
407
- const compiled = compileRules(this.source, workspaceRoot);
408
- this.compiled.set(workspaceRoot, compiled);
471
+ const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
472
+ this.compiled.set(key, compiled);
409
473
  return compiled;
410
474
  }
411
475
  };
412
476
  //#endregion
413
- //#region src/config.ts
414
- const Config = z.object({
415
- access: z.union([z.array(z.string()), z.string()]).default([]),
416
- runnerCommand: z.array(z.string()).default([]),
417
- runnerFailureSignatures: z.array(z.string()).default([]),
418
- probeTimeoutMs: z.natural().default(5e3),
419
- cwd: z.string().default(process.cwd()),
420
- diffBasisMaxBytes: z.number().default(10485760)
421
- });
422
- //#endregion
423
477
  //#region src/index.ts
424
478
  const name = "sandbox-local";
425
479
  const inject = ["sandboxPolicy"];
@@ -433,7 +487,7 @@ function warnAboutDegradedRules(ctx, rules) {
433
487
  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");
434
488
  }
435
489
  function apply(ctx, config) {
436
- const rules = ruleSourceOf(config, process.env);
490
+ const rules = ruleSourceOf(config.access.get(), process.env);
437
491
  warnAboutDegradedRules(ctx, rules);
438
492
  new ConfigurableSandboxProvider(ctx, config);
439
493
  new ConfigurableFileSystem(ctx, config);
@@ -0,0 +1,23 @@
1
+ //#region src/rows.d.ts
2
+ /**
3
+ * 本包作为能力包发布的**装配数据**:行本身在部署那一层装(`packages/bundles/sandbox-profile` 引用它渲染 patch)。
4
+ *
5
+ * 官方 `sandbox`(进程沙箱)与 `fs-sandbox`(文件围栏)两个服务只能有一份实现,所以那两行禁掉、换成
6
+ * 本包那一行;`config.access` 的值由部署那层给(schema 默认是空规则)。
7
+ */
8
+ /** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
9
+ interface PatchRow {
10
+ readonly id?: string;
11
+ readonly disabled?: boolean;
12
+ readonly insert?: readonly RowEntry[];
13
+ }
14
+ /** `insert` 里的一个条目。 */
15
+ interface RowEntry {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly config?: Readonly<Record<string, unknown>>;
19
+ }
20
+ /** 沙箱替换的装配行:禁官方两行 + 插本包一行(值由部署那层按 id 覆盖)。 */
21
+ declare function sandboxRows(): readonly PatchRow[];
22
+ //#endregion
23
+ export { PatchRow, RowEntry, sandboxRows };
@@ -0,0 +1,23 @@
1
+ //#region src/rows.d.ts
2
+ /**
3
+ * 本包作为能力包发布的**装配数据**:行本身在部署那一层装(`packages/bundles/sandbox-profile` 引用它渲染 patch)。
4
+ *
5
+ * 官方 `sandbox`(进程沙箱)与 `fs-sandbox`(文件围栏)两个服务只能有一份实现,所以那两行禁掉、换成
6
+ * 本包那一行;`config.access` 的值由部署那层给(schema 默认是空规则)。
7
+ */
8
+ /** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
9
+ interface PatchRow {
10
+ readonly id?: string;
11
+ readonly disabled?: boolean;
12
+ readonly insert?: readonly RowEntry[];
13
+ }
14
+ /** `insert` 里的一个条目。 */
15
+ interface RowEntry {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly config?: Readonly<Record<string, unknown>>;
19
+ }
20
+ /** 沙箱替换的装配行:禁官方两行 + 插本包一行(值由部署那层按 id 覆盖)。 */
21
+ declare function sandboxRows(): readonly PatchRow[];
22
+ //#endregion
23
+ export { PatchRow, RowEntry, sandboxRows };
package/dist/rows.mjs ADDED
@@ -0,0 +1,20 @@
1
+ //#region src/rows.ts
2
+ /** 沙箱替换的装配行:禁官方两行 + 插本包一行(值由部署那层按 id 覆盖)。 */
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
+ export { sandboxRows };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/dsh-sandbox-local",
3
- "version": "0.0.3-alpha.8",
3
+ "version": "0.0.3-alpha.9",
4
4
  "description": "Configurable sandbox bundle: replaces the shipped process-sandbox provider and filesystem fence with implementations that add extra writable roots, read-only roots and access denials on top of the upstream semantics.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -18,45 +18,61 @@
18
18
  "locale/*.json",
19
19
  "dist",
20
20
  "src",
21
- "cordis.patch.yml",
22
21
  "!**/__tests__"
23
22
  ],
24
23
  "type": "module",
25
24
  "exports": {
26
25
  ".": "./dist/index.mjs",
26
+ "./rows": "./dist/rows.mjs",
27
27
  "./package.json": "./package.json",
28
- "./cordis.patch.yml": "./cordis.patch.yml",
28
+ "./client": {
29
+ "types": "./dist/client.d.cts",
30
+ "default": "./dist/client.cjs"
31
+ },
29
32
  "./locale/*.json": "./locale/*.json"
30
33
  },
31
34
  "devDependencies": {
32
35
  "@deepseek-ai/cordis": "4.0.4",
33
- "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
34
- "@deepseek-ai/dsh-fs": "0.1.7-rc.1",
35
- "@deepseek-ai/dsh-fs-local": "0.1.7-rc.1",
36
- "@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.1",
37
- "@deepseek-ai/dsh-sandbox": "0.1.7-rc.1",
38
- "@deepseek-ai/dsh-sandbox-local": "0.1.7-rc.1",
39
- "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.1",
40
- "@deepseek-ai/dsh-scope": "0.1.7-rc.1",
41
- "@deepseek-ai/dsh-session": "0.1.7-rc.1",
42
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1"
36
+ "@deepseek-ai/cosmokit": "1.8.5",
37
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
38
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
39
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
40
+ "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
41
+ "@deepseek-ai/dsh-fs-local": "0.1.7-rc.2",
42
+ "@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.2",
43
+ "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
44
+ "@deepseek-ai/dsh-sandbox-local": "0.1.7-rc.2",
45
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
46
+ "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
47
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
48
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
49
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.9"
43
50
  },
44
51
  "peerDependencies": {
45
- "@deepseek-ai/cordis": "4.0.4",
46
- "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
47
- "@deepseek-ai/dsh-fs": "0.1.7-rc.1",
48
- "@deepseek-ai/dsh-fs-local": "0.1.7-rc.1",
49
- "@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.1",
50
- "@deepseek-ai/dsh-sandbox": "0.1.7-rc.1",
51
- "@deepseek-ai/dsh-sandbox-local": "0.1.7-rc.1",
52
- "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.1",
53
- "@deepseek-ai/dsh-session": "0.1.7-rc.1",
54
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
55
- "@deepseek-ai/schemastery": "3.18.4"
52
+ "@deepseek-ai/cordis": "^4.0.4",
53
+ "@deepseek-ai/cosmokit": "^1.8.5",
54
+ "@deepseek-ai/dsh-agent": "^0.1.7-rc.2",
55
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
56
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
57
+ "@deepseek-ai/dsh-fs": "^0.1.7-rc.2",
58
+ "@deepseek-ai/dsh-fs-local": "^0.1.7-rc.2",
59
+ "@deepseek-ai/dsh-fs-sandbox": "^0.1.7-rc.2",
60
+ "@deepseek-ai/dsh-sandbox": "^0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-sandbox-local": "^0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.7-rc.2",
63
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
64
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.2",
65
+ "@deepseek-ai/schemastery": "^3.18.4",
66
+ "@morlay/dsh-client-ui-primitives": "0.0.2-alpha.9"
56
67
  },
57
68
  "dsh": {
58
- "bundle": {
59
- "patch": "./cordis.patch.yml"
69
+ "client": {
70
+ "platform": "web",
71
+ "inject": [
72
+ "@deepseek-ai/dsh-client-locale",
73
+ "@deepseek-ai/dsh-client-ui-slots",
74
+ "@morlay/dsh-client-ui-primitives"
75
+ ]
60
76
  }
61
77
  },
62
78
  "scripts": {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * client 半:这一行的配置页由通用 schema 表单按 volatile 字段生成(`access` 是页面可改的那一项,改完规则
3
+ * 当场重算),本包只把键名换成中文标签与说明。
4
+ */
5
+
6
+ import type { Context } from "@deepseek-ai/cordis";
7
+ import type {} from "@deepseek-ai/dsh-client-locale/client";
8
+ import type {} from "@deepseek-ai/dsh-client-ui-slots";
9
+ import { en, zh, type SandboxFieldLocaleKey } from "./locales.ts";
10
+
11
+ /** 本包 host 行 id:行配置页读的就是这个命名空间。 */
12
+ export const SANDBOX_NS = "sandbox-local";
13
+
14
+ /** 这一行里需要中文文案的字段。 */
15
+ const FIELDS = ["access"] as const;
16
+
17
+ /** 本页字典的命名空间。 */
18
+ export const NS = "settings.sandbox-local";
19
+
20
+ declare module "@deepseek-ai/dsh-client-ui-slots" {
21
+ interface LocaleNamespaceMap {
22
+ /** 设置页里这一行的字段文案。 */
23
+ "settings.sandbox-local": SandboxFieldLocaleKey;
24
+ }
25
+ }
26
+
27
+ /** 需要的服务:槽位(字段槽)与字典。 */
28
+ export const inject = ["locale"];
29
+
30
+ /**
31
+ * 给 `access` 补文案。
32
+ * @param ctx - 浏览器插件上下文。
33
+ */
34
+ export function apply(ctx: Context): void {
35
+ const t = ctx.locale.bind(NS);
36
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "sandbox-local: field locale");
37
+ // 提示面由行配置表单提供:等它可用再注册(`ctx.get` 在它还没提供时拿不到,注册会被静静跳过)。
38
+ ctx.inject(["schemaFormHints"], (scope) =>
39
+ scope.effect(() => {
40
+ const hints = scope.schemaFormHints;
41
+ const offs = FIELDS.map((key) =>
42
+ hints.describe(SANDBOX_NS, [key], () => ({
43
+ label: t(key),
44
+ hint: t("accessHint"),
45
+ })),
46
+ );
47
+ return () => {
48
+ for (const off of offs) off();
49
+ };
50
+ }, "sandbox-local: field wording"),
51
+ );
52
+ }
@@ -0,0 +1,15 @@
1
+ /** 本包在配置页上的字段文案(命名空间 `settings.sandbox-local`)。 */
2
+
3
+ export const zh = {
4
+ access: "额外可写根与拒绝项",
5
+ accessHint:
6
+ "一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;支持 `~`、环境变量与 glob。改完当场生效。",
7
+ } as const;
8
+
9
+ export const en: Record<keyof typeof zh, string> = {
10
+ access: "Extra writable roots and refusals",
11
+ accessHint:
12
+ "One entry per line: `rw:<path>` grants a writable root, `r-:<path>` makes it read-only, `--:<path>` denies it; `~`, env templates, and globs work. Edits apply immediately.",
13
+ };
14
+
15
+ export type SandboxFieldLocaleKey = keyof typeof zh;
package/src/config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Volatile } from "@deepseek-ai/cosmokit";
1
2
  import z from "@deepseek-ai/schemastery";
2
3
  import type { Config as UpstreamFsConfig } from "@deepseek-ai/dsh-fs-local";
3
4
  import type { Config as UpstreamSandboxConfig } from "@deepseek-ai/dsh-sandbox-local";
@@ -6,11 +7,123 @@ export interface Config extends UpstreamSandboxConfig, UpstreamFsConfig {
6
7
  access?: string | string[];
7
8
  }
8
9
 
9
- export const Config: z<Config> = z.object({
10
- access: z.union([z.array(z.string()), z.string()]).default([]),
11
- runnerCommand: z.array(z.string()).default([]),
12
- runnerFailureSignatures: z.array(z.string()).default([]),
13
- probeTimeoutMs: z.natural().default(5_000),
14
- cwd: z.string().default(process.cwd()),
15
- diffBasisMaxBytes: z.number().default(10 * 1024 * 1024),
10
+ /**
11
+ * schema 解析之后的形状:`access` 是 **volatile 稳定引用**(页面可改的那一项),读它要过 `.get()`。
12
+ *
13
+ * 其余字段也是 volatile,但页面把它们画成**只读**(schema 上的 `disabled()`):它们是装配事实——runner 命令、
14
+ * 进程 cwd、上游基类读一次的超时与差额上限——放在页面上是为了看清「这一行现在装配成什么样」,改它们要重挂
15
+ * 这一行。所以给上游基类的那份配置是**解包后的值**({@link upstreamConfigOf}):基类读的是值,不是引用。
16
+ */
17
+ export interface ResolvedConfig extends Omit<
18
+ Config,
19
+ | "access"
20
+ | "runnerCommand"
21
+ | "runnerFailureSignatures"
22
+ | "probeTimeoutMs"
23
+ | "cwd"
24
+ | "diffBasisMaxBytes"
25
+ > {
26
+ readonly access: Volatile<string | string[]>;
27
+ readonly runnerCommand: Volatile<readonly string[]>;
28
+ readonly runnerFailureSignatures: Volatile<readonly string[]>;
29
+ readonly probeTimeoutMs: Volatile<number>;
30
+ readonly cwd: Volatile<string>;
31
+ readonly diffBasisMaxBytes: Volatile<number>;
32
+ }
33
+
34
+ /**
35
+ * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
36
+ * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
37
+ */
38
+ const localized = (text: { zh: string; en: string }): string => text as unknown as string;
39
+
40
+ export const Config: z<Config, ResolvedConfig> = z.object({
41
+ access: z
42
+ .union([z.array(z.string()), z.string()])
43
+ .default([])
44
+ .description(
45
+ localized({
46
+ zh:
47
+ "额外可写根与拒绝项,一行一条:`rw:<路径>` 追加可写根,`r-:<路径>` 只读,`--:<路径>` 拒绝;" +
48
+ "支持 `~`、环境变量与 glob。改它当场生效(沙箱规则每次按当前值重算),不用重挂这一行。",
49
+ en:
50
+ "Extra writable roots and refusals, one entry per line: `rw:<path>` grants a writable root, " +
51
+ "`r-:<path>` makes it read-only, `--:<path>` denies it; `~`, env templates, and globs are supported. " +
52
+ "Edits take effect on the next rule compile without remounting the row.",
53
+ }),
54
+ )
55
+ .volatile(),
56
+ runnerCommand: z
57
+ .array(z.string())
58
+ .default([])
59
+ .disabled()
60
+ .description(
61
+ localized({
62
+ zh: "沙箱 runner 的启动命令;空数组表示按平台自带的链去选。改它要重挂这一行。",
63
+ en: "Runner command for confined subprocesses; an empty array picks the platform chain. Edits apply on remount.",
64
+ }),
65
+ )
66
+ .volatile(),
67
+ runnerFailureSignatures: z
68
+ .array(z.string())
69
+ .default([])
70
+ .disabled()
71
+ .description(
72
+ localized({
73
+ zh: "runner 命令「用不了」的判定片段,与 `runnerCommand` 成对出现。",
74
+ en: "Fragments that mark the runner command unusable; declared together with `runnerCommand`.",
75
+ }),
76
+ )
77
+ .volatile(),
78
+ probeTimeoutMs: z
79
+ .natural()
80
+ .default(5_000)
81
+ .disabled()
82
+ .description(
83
+ localized({
84
+ zh: "探测 runner 可用性的超时(毫秒)。",
85
+ en: "Timeout in ms for probing whether the runner works.",
86
+ }),
87
+ )
88
+ .volatile(),
89
+ cwd: z
90
+ .string()
91
+ .default(process.cwd())
92
+ .disabled()
93
+ .description(
94
+ localized({
95
+ zh: "相对路径的基准目录;默认是启动这个进程时的工作目录。",
96
+ en: "Base directory for relative paths; defaults to the process working directory.",
97
+ }),
98
+ )
99
+ .volatile(),
100
+ diffBasisMaxBytes: z
101
+ .number()
102
+ .default(10 * 1024 * 1024)
103
+ .disabled()
104
+ .description(
105
+ localized({
106
+ zh: "覆盖写差异的单侧字节上限(默认 10 MiB)。",
107
+ en: "Per-side byte cap for overwrite diffs (10 MiB by default).",
108
+ }),
109
+ )
110
+ .volatile(),
16
111
  });
112
+
113
+ /**
114
+ * 交给上游基类的那份配置:装配事实取当前值。
115
+ *
116
+ * 上游 `LocalSandboxProvider` / `LocalFileSystem` 在构造时读一次这些字段并做装配期校验,所以它们要的是值;
117
+ * 设置页改它们走的是「写盘 → Loader 重挂这一行 → 重新解析」这条路。
118
+ * @param config - 本行解析后的配置。
119
+ * @returns 上游两个基类认的普通配置对象。
120
+ */
121
+ export function upstreamConfigOf(config: ResolvedConfig): UpstreamSandboxConfig & UpstreamFsConfig {
122
+ return {
123
+ runnerCommand: [...config.runnerCommand.get()],
124
+ runnerFailureSignatures: [...config.runnerFailureSignatures.get()],
125
+ probeTimeoutMs: config.probeTimeoutMs.get(),
126
+ cwd: config.cwd.get(),
127
+ diffBasisMaxBytes: config.diffBasisMaxBytes.get(),
128
+ };
129
+ }
package/src/fs.ts CHANGED
@@ -9,7 +9,7 @@ import type {
9
9
  FsWriteOutcome,
10
10
  } from "@deepseek-ai/dsh-fs";
11
11
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
12
- import type { Config } from "./config.ts";
12
+ import { upstreamConfigOf, type ResolvedConfig } from "./config.ts";
13
13
  import type { SandboxExecutionPolicy, SandboxMode } from "@deepseek-ai/dsh-sandbox";
14
14
  import type {} from "@deepseek-ai/dsh-sandbox-policy";
15
15
  import { isPathUnder } from "./containment.ts";
@@ -27,14 +27,19 @@ export class ConfigurableFileSystem extends LocalFileSystem {
27
27
  static inject = ["sandboxPolicy"];
28
28
 
29
29
  private readonly defaultMode: SandboxMode;
30
- private readonly source: RuleSource;
30
+ private readonly access: ResolvedConfig["access"];
31
31
 
32
32
  private readonly compiled = new Map<string, CompiledRules>();
33
33
 
34
- constructor(ctx: Context, config: Config) {
35
- super(ctx, config);
34
+ constructor(ctx: Context, config: ResolvedConfig) {
35
+ super(ctx, upstreamConfigOf(config));
36
36
  this.defaultMode = ctx.sandboxPolicy.defaultMode;
37
- this.source = ruleSourceOf(config, process.env);
37
+ this.access = config.access;
38
+ }
39
+
40
+ /** 规则源现场重算:`access` 是 volatile 引用,页面改完下一次编译就是新规则。 */
41
+ private get source(): RuleSource {
42
+ return ruleSourceOf(this.access.get(), process.env);
38
43
  }
39
44
 
40
45
  override get sandboxMode(): SandboxMode {
@@ -129,11 +134,14 @@ export class ConfigurableFileSystem extends LocalFileSystem {
129
134
  );
130
135
  }
131
136
 
137
+ /** 编译结果按「工作区 + 当前 access」缓存:access 是页面可改的引用,改了就是另一份规则。 */
132
138
  private rulesFor(workspaceRoot: string): CompiledRules {
133
- const cached = this.compiled.get(workspaceRoot);
139
+ const access = this.access.get();
140
+ const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
141
+ const cached = this.compiled.get(key);
134
142
  if (cached !== undefined) return cached;
135
- const compiled = compileRules(this.source, workspaceRoot);
136
- this.compiled.set(workspaceRoot, compiled);
143
+ const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
144
+ this.compiled.set(key, compiled);
137
145
  return compiled;
138
146
  }
139
147
  }
package/src/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
- import type { Config } from "./config.ts";
2
+ import type { ResolvedConfig } from "./config.ts";
3
3
  import { ConfigurableFileSystem } from "./fs.ts";
4
4
  import { installPolicyContext } from "./policy.ts";
5
5
  import { ruleSourceOf, type RuleSource } from "./rules.ts";
6
6
  import { ConfigurableSandboxProvider } from "./sandbox.ts";
7
7
 
8
8
  export { Config } from "./config.ts";
9
+ export type { ResolvedConfig } from "./config.ts";
9
10
 
10
11
  export const name = "sandbox-local";
11
12
 
@@ -33,8 +34,8 @@ function warnAboutDegradedRules(ctx: Context, rules: RuleSource): void {
33
34
  }
34
35
  }
35
36
 
36
- export function apply(ctx: Context, config: Config): void {
37
- const rules = ruleSourceOf(config, process.env);
37
+ export function apply(ctx: Context, config: ResolvedConfig): void {
38
+ const rules = ruleSourceOf(config.access.get(), process.env);
38
39
  warnAboutDegradedRules(ctx, rules);
39
40
  new ConfigurableSandboxProvider(ctx, config);
40
41
  new ConfigurableFileSystem(ctx, config);
package/src/rows.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 本包作为能力包发布的**装配数据**:行本身在部署那一层装(`packages/bundles/sandbox-profile` 引用它渲染 patch)。
3
+ *
4
+ * 官方 `sandbox`(进程沙箱)与 `fs-sandbox`(文件围栏)两个服务只能有一份实现,所以那两行禁掉、换成
5
+ * 本包那一行;`config.access` 的值由部署那层给(schema 默认是空规则)。
6
+ */
7
+
8
+ /** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
9
+ export interface PatchRow {
10
+ readonly id?: string;
11
+ readonly disabled?: boolean;
12
+ readonly insert?: readonly RowEntry[];
13
+ }
14
+
15
+ /** `insert` 里的一个条目。 */
16
+ export interface RowEntry {
17
+ readonly id: string;
18
+ readonly name: string;
19
+ readonly config?: Readonly<Record<string, unknown>>;
20
+ }
21
+
22
+ /** 沙箱替换的装配行:禁官方两行 + 插本包一行(值由部署那层按 id 覆盖)。 */
23
+ export function sandboxRows(): readonly PatchRow[] {
24
+ return [
25
+ { id: "sandbox", disabled: true },
26
+ { id: "fs-sandbox", disabled: true },
27
+ { insert: [{ id: "sandbox-local", name: "@morlay/dsh-sandbox-local" }] },
28
+ ];
29
+ }
package/src/rules.ts CHANGED
@@ -132,10 +132,10 @@ export function parseAccess(input: string | readonly string[] | undefined): Rule
132
132
  }
133
133
 
134
134
  export function ruleSourceOf(
135
- config: { access?: string | readonly string[] },
135
+ access: string | readonly string[] | undefined,
136
136
  env: NodeJS.ProcessEnv,
137
137
  ): RuleSource {
138
- const parsed = parseAccess(config.access);
138
+ const parsed = parseAccess(access);
139
139
  const expand = (values: readonly string[]): string[] =>
140
140
  values.map((value) => expandEnvTemplates(value, env));
141
141
  return {
package/src/sandbox.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  import type { ConfinedArgv, SandboxPolicy } from "@deepseek-ai/dsh-sandbox";
3
3
  import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
4
- import type { Config } from "./config.ts";
4
+ import { upstreamConfigOf, type ResolvedConfig } from "./config.ts";
5
5
  import { extendConfinedArgv } from "./dialects.ts";
6
6
  import {
7
7
  compileRules,
@@ -13,13 +13,19 @@ import {
13
13
  } from "./rules.ts";
14
14
 
15
15
  export class ConfigurableSandboxProvider extends LocalSandboxProvider {
16
- private readonly source: RuleSource;
16
+ private readonly access: ResolvedConfig["access"];
17
17
 
18
18
  private readonly compiled = new Map<string, CompiledRules>();
19
19
 
20
- constructor(ctx: Context, config: Config) {
21
- super(ctx, config);
22
- this.source = ruleSourceOf(config, process.env);
20
+ constructor(ctx: Context, config: ResolvedConfig) {
21
+ // 上游基类读的是**值**:装配事实在页面上只读可见,改它们要重挂这一行(重挂后重新解析)。
22
+ super(ctx, upstreamConfigOf(config));
23
+ this.access = config.access;
24
+ }
25
+
26
+ /** 规则源现场重算:`access` 是 volatile 引用,页面改完下一次编译就是新规则。 */
27
+ private get source(): RuleSource {
28
+ return ruleSourceOf(this.access.get(), process.env);
23
29
  }
24
30
 
25
31
  override async confine(
@@ -34,11 +40,14 @@ export class ConfigurableSandboxProvider extends LocalSandboxProvider {
34
40
  return { ...confined, argv: extendConfinedArgv(confined.argv, effective) };
35
41
  }
36
42
 
43
+ /** 编译结果按「工作区 + 当前 access」缓存:access 是页面可改的引用,改了就是另一份规则。 */
37
44
  private rulesFor(workspaceRoot: string): CompiledRules {
38
- const cached = this.compiled.get(workspaceRoot);
45
+ const access = this.access.get();
46
+ const key = `${workspaceRoot}\u0000${JSON.stringify(access ?? null)}`;
47
+ const cached = this.compiled.get(key);
39
48
  if (cached !== undefined) return cached;
40
- const compiled = compileRules(this.source, workspaceRoot);
41
- this.compiled.set(workspaceRoot, compiled);
49
+ const compiled = compileRules(ruleSourceOf(access, process.env), workspaceRoot);
50
+ this.compiled.set(key, compiled);
42
51
  return compiled;
43
52
  }
44
53
  }
package/cordis.patch.yml DELETED
@@ -1,24 +0,0 @@
1
- # @morlay/dsh-sandbox-local bundle patch:把官方沙箱栈换成可配置实现。
2
- #
3
- # - `sandbox`:官方 `@deepseek-ai/dsh-sandbox-local` 注册 `ctx.sandbox`;本包用它的
4
- # 子类注册同名服务(复用官方 probe / runner 选择 / Windows ACL 流程,只在
5
- # 选中的 runner 参数上追加 `access` 规则)。
6
- # - `fs-sandbox`:官方 `@deepseek-ai/dsh-fs-sandbox` 注册 `ctx.fs`;本包用继承
7
- # `LocalFileSystem` 的实现注册同名服务(复用同一套写围栏,并让 `--` 条目同时
8
- # 拦住读与写、`r-` / `rw` 条目参与写判定)。
9
- #
10
- # 这组 patch 供把本包作为独立 bundle 采用的部署使用:行不带 config,schema 默认是空规则。
11
- # 行不带 config(schema 默认是空规则):规则的**值**由配置层给——本部署由 `@morlay/dsh-profile`
12
- # 按 id 做 config 覆盖。两层都住 app 的 `dsh.profile.bundles`,本包排在前面(配置层要能找到这一行)。
13
- #
14
- # 若别处再自行"禁官方两行 + 插入本行",就会与本 bundle 重复插入同一行——采用方式二选一。
15
-
16
- - id: sandbox
17
- disabled: true
18
-
19
- - id: fs-sandbox
20
- disabled: true
21
-
22
- - insert:
23
- - id: sandbox-local
24
- name: "@morlay/dsh-sandbox-local"