@morlay/dsh-sandbox-local 0.0.2

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.
@@ -0,0 +1,72 @@
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
+ import type { BigIntStats } from "node:fs";
11
+ import { stat } from "node:fs/promises";
12
+ import { dirname, sep } from "node:path";
13
+
14
+ /** 视为“路径不存在”的错误码:只有它们能让祖先遍历继续。 */
15
+ const MISSING_CODES: ReadonlySet<string> = new Set(["ENOENT", "ENOTDIR"]);
16
+
17
+ /** stat 一次,缺失返回 undefined;其它失败(权限、I/O)继续抛出。 */
18
+ async function statIfPresent(path: string): Promise<BigIntStats | undefined> {
19
+ try {
20
+ return await stat(path, { bigint: true });
21
+ } catch (error) {
22
+ const code = (error as NodeJS.ErrnoException).code;
23
+ if (code !== undefined && MISSING_CODES.has(code)) return undefined;
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ /** 按大小写敏感性归一用于比较的拼写。 */
29
+ function comparablePath(path: string, caseSensitive: boolean): string {
30
+ return caseSensitive ? path : path.toLowerCase();
31
+ }
32
+
33
+ /** 词法包含判定(目标可带尚不存在的后缀)。 */
34
+ function isLexicallyUnder(path: string, root: string, caseSensitive: boolean): boolean {
35
+ const comparableTarget = comparablePath(path, caseSensitive);
36
+ const comparableRoot = comparablePath(root, caseSensitive);
37
+ if (comparableTarget === comparableRoot) return true;
38
+ const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
39
+ return comparableTarget.startsWith(prefix);
40
+ }
41
+
42
+ /** 两次 stat 是否指向同一个文件系统对象。 */
43
+ function sameIdentity(left: BigIntStats, right: BigIntStats): boolean {
44
+ return left.dev === right.dev && left.ino === right.ino;
45
+ }
46
+
47
+ /**
48
+ * 判断 canonical 目标是否就是某个根或位于其下。
49
+ * 拼写不同时(Windows 长名/8.3 别名、大小写差异)沿目标的现存祖先比较文件系统身份,
50
+ * 不把包含判定弱化成文本近似。
51
+ * @param path - 目标的 canonical 路径(可带尚不存在的尾部)。
52
+ * @param root - canonical 可写根。
53
+ * @param caseSensitive - 词法比较是否区分大小写;默认按宿主平台约定。
54
+ * @returns 目标是该根或其后代时为 true。
55
+ */
56
+ export async function isPathUnder(
57
+ path: string,
58
+ root: string,
59
+ caseSensitive = process.platform !== "win32",
60
+ ): Promise<boolean> {
61
+ if (isLexicallyUnder(path, root, caseSensitive)) return true;
62
+ const rootInfo = await statIfPresent(root);
63
+ if (rootInfo === undefined) return false;
64
+ let ancestor = path;
65
+ for (;;) {
66
+ const ancestorInfo = await statIfPresent(ancestor);
67
+ if (ancestorInfo !== undefined && sameIdentity(ancestorInfo, rootInfo)) return true;
68
+ const parent = dirname(ancestor);
69
+ if (parent === ancestor) return false;
70
+ ancestor = parent;
71
+ }
72
+ }
@@ -0,0 +1,158 @@
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
+ import { isEmptyRules, type CompiledRules } from "./rules.ts";
14
+
15
+ /** 官方 provider 可选的执行方言。 */
16
+ export type SandboxDialect = "seatbelt" | "bwrap" | "landlock" | "windows-acl";
17
+
18
+ /** runner 部分与调用方 argv 的分隔符。 */
19
+ const SEPARATOR = "--";
20
+
21
+ /**
22
+ * 一个方言能表达的规则能力。
23
+ * `readOnly` 指 `r-` 条目(只拒写入);`denyWriteOnly` 指 `--` 条目在该方言上只能
24
+ * 退化为“只拒写入”(读取仍放行)。
25
+ */
26
+ export interface DialectCapabilities {
27
+ /** 能否把额外可写根写进 runner 参数。 */
28
+ readonly allowWrite: boolean;
29
+ /** 能否表达 `r-`(只拒写入)。 */
30
+ readonly readOnly: boolean;
31
+ /** 能否表达 `--` 的完整语义(读与写都拒)。 */
32
+ readonly denyReadWrite: boolean;
33
+ /** `--` 是否只能退化为拒绝写入。 */
34
+ readonly denyWriteOnly: boolean;
35
+ }
36
+
37
+ /** 各方言的规则表达能力——加载期据此告警,运行期据此决定是否改写参数。 */
38
+ export const DIALECT_CAPABILITIES: Record<SandboxDialect, DialectCapabilities> = {
39
+ seatbelt: { allowWrite: true, readOnly: true, denyReadWrite: true, denyWriteOnly: false },
40
+ bwrap: { allowWrite: true, readOnly: true, denyReadWrite: false, denyWriteOnly: true },
41
+ landlock: { allowWrite: true, readOnly: false, denyReadWrite: false, denyWriteOnly: false },
42
+ "windows-acl": { allowWrite: false, readOnly: false, denyReadWrite: false, denyWriteOnly: false },
43
+ };
44
+
45
+ /** runner 部分的结束位置。 */
46
+ function separatorIndex(argv: readonly string[]): number {
47
+ const index = argv.indexOf(SEPARATOR);
48
+ if (index === -1) {
49
+ throw new Error("sandbox rules: the confined argv carries no `--` separator to extend");
50
+ }
51
+ return index;
52
+ }
53
+
54
+ /**
55
+ * 识别 argv 使用的执行方言。
56
+ * @param argv - 官方 provider 返回的完整 confined argv。
57
+ * @returns 方言,或无法识别时的 `undefined`(例如运维自定义的 runnerCommand)。
58
+ */
59
+ export function dialectOf(argv: readonly string[]): SandboxDialect | undefined {
60
+ const separator = argv.indexOf(SEPARATOR);
61
+ const runner = separator === -1 ? argv : argv.slice(0, separator);
62
+ if (runner.includes("-p")) return "seatbelt";
63
+ if (runner[0] === "bwrap") return "bwrap";
64
+ if (runner.includes("--workspace")) return "windows-acl";
65
+ if (runner.includes("--ro") || runner.includes("--rw")) return "landlock";
66
+ return undefined;
67
+ }
68
+
69
+ /** SBPL 字符串字面量。 */
70
+ function sbplString(value: string): string {
71
+ return `"${value.replaceAll("\\", String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
72
+ }
73
+
74
+ /**
75
+ * SBPL `#"…"` 的正则体:正则自身的 `\` 必须保留(glob 翻译用它转义元字符),
76
+ * 因此拒绝项里出现双引号时直接报错,而不是产出一个含义变化的 profile。
77
+ */
78
+ function sbplRegexBody(source: string): string {
79
+ if (source.includes('"')) {
80
+ throw new Error(
81
+ `sandbox rules: deny pattern ${JSON.stringify(source)} cannot be expressed in a Seatbelt profile`,
82
+ );
83
+ }
84
+ return source;
85
+ }
86
+
87
+ /** Seatbelt:把 allow / deny 规则追加到 `-p` 的 profile 文本末尾。 */
88
+ function extendSeatbelt(argv: readonly string[], rules: CompiledRules): string[] {
89
+ const index = argv.indexOf("-p");
90
+ const profile = index === -1 ? undefined : argv[index + 1];
91
+ if (profile === undefined) {
92
+ throw new Error("sandbox rules: the Seatbelt runner argv carries no `-p` profile to extend");
93
+ }
94
+ const additions = [
95
+ ...rules.allowRoots.map((root) => `(allow file-write* (subpath ${sbplString(root)}))`),
96
+ ...rules.readOnlySubtrees.map((path) => `(deny file-write* (subpath ${sbplString(path)}))`),
97
+ ...rules.readOnlyPatterns.map(
98
+ (pattern) => `(deny file-write* (regex #"${sbplRegexBody(pattern.source)}"))`,
99
+ ),
100
+ ...rules.denySubtrees.map(
101
+ (path) => `(deny file-read* file-write* (subpath ${sbplString(path)}))`,
102
+ ),
103
+ ...rules.denyPatterns.map(
104
+ (pattern) => `(deny file-read* file-write* (regex #"${sbplRegexBody(pattern.source)}"))`,
105
+ ),
106
+ ];
107
+ const extended = `${profile} ${additions.join(" ")}`;
108
+ return [...argv.slice(0, index + 1), extended, ...argv.slice(index + 2)];
109
+ }
110
+
111
+ /**
112
+ * bwrap:额外可写根用 `--bind-try`(路径不存在时跳过);`r-` 与 `--` 都用
113
+ * `--ro-bind-try` 覆盖成只读(挂载后行覆盖前行)——也就是说 `--` 在 bwrap 上退化为
114
+ * “只拒写入”。glob 模式无法表达为静态挂载,由调用方在加载期告警。
115
+ */
116
+ function extendBwrap(argv: readonly string[], rules: CompiledRules): string[] {
117
+ const separator = separatorIndex(argv);
118
+ const additions: string[] = [];
119
+ for (const root of rules.allowRoots) additions.push("--bind-try", root, root);
120
+ for (const path of [...rules.readOnlySubtrees, ...rules.denySubtrees]) {
121
+ additions.push("--ro-bind-try", path, path);
122
+ }
123
+ return [...argv.slice(0, separator), ...additions, ...argv.slice(separator)];
124
+ }
125
+
126
+ /** Landlock:只能追加可写授权(`--rw`),`r-` 与 `--` 都无对应表达。 */
127
+ function extendLandlock(argv: readonly string[], rules: CompiledRules): string[] {
128
+ const separator = separatorIndex(argv);
129
+ const additions = rules.allowRoots.flatMap((root) => ["--rw", root]);
130
+ return [...argv.slice(0, separator), ...additions, ...argv.slice(separator)];
131
+ }
132
+
133
+ /**
134
+ * 按 argv 的方言追加规则。
135
+ * @param argv - 官方 provider 返回的 confined argv。
136
+ * @param rules - 已编译规则;空规则原样返回。
137
+ * @returns 追加规则后的 argv;方言无法识别时抛错(规则不能静默失效)。
138
+ */
139
+ export function extendConfinedArgv(argv: readonly string[], rules: CompiledRules): string[] {
140
+ if (isEmptyRules(rules)) return [...argv];
141
+ const dialect = dialectOf(argv);
142
+ if (dialect === undefined) {
143
+ throw new Error(
144
+ "sandbox rules: cannot extend an unrecognized sandbox runner argv; drop allowWrite/deny or configure a bwrap-compatible runnerCommand",
145
+ );
146
+ }
147
+ switch (dialect) {
148
+ case "seatbelt":
149
+ return extendSeatbelt(argv, rules);
150
+ case "bwrap":
151
+ return extendBwrap(argv, rules);
152
+ case "landlock":
153
+ return extendLandlock(argv, rules);
154
+ case "windows-acl":
155
+ // windows-acl 的 runner 参数没有承载额外 grant 的入口:加载期已告警,这里保持原 argv。
156
+ return [...argv];
157
+ }
158
+ }
package/src/fs.ts ADDED
@@ -0,0 +1,190 @@
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
+ import type { Context } from "@deepseek-ai/cordis";
13
+ import { FsError } from "@deepseek-ai/dsh-fs";
14
+ import type {
15
+ FsEditOutcome,
16
+ FsEditRequest,
17
+ FsTarget,
18
+ FsVersion,
19
+ FsWriteIntent,
20
+ FsWriteOutcome,
21
+ } from "@deepseek-ai/dsh-fs";
22
+ import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
23
+ import type { Config } from "./config.ts";
24
+ import { writableRoots } from "@deepseek-ai/dsh-sandbox";
25
+ import type { SandboxExecutionPolicy, SandboxMode } from "@deepseek-ai/dsh-sandbox";
26
+ import type {} from "@deepseek-ai/dsh-sandbox-policy";
27
+ import { isPathUnder } from "./containment.ts";
28
+ import {
29
+ compileRules,
30
+ isDenied,
31
+ isReadOnly,
32
+ ruleSourceOf,
33
+ type CompiledRules,
34
+ type RuleSource,
35
+ } from "./rules.ts";
36
+
37
+ /**
38
+ * 官方本机文件系统后端的可配置版本。
39
+ * `--` 条目在解析阶段拒绝目标(覆盖 read / write / edit / list 等一切工具入口),
40
+ * `r-` 与 `--` 条目在写入前拒绝写入(优先于任何可写根),写操作再按
41
+ * `workspace-write` + `rw` 条目复核 containment。
42
+ */
43
+ export class ConfigurableFileSystem extends LocalFileSystem {
44
+ static inject = ["sandboxPolicy"];
45
+
46
+ private readonly defaultMode: SandboxMode;
47
+ private readonly source: RuleSource;
48
+ /** 规则按工作区根编译一次(相对规则相对该调用的工作区)。 */
49
+ private readonly compiled = new Map<string, CompiledRules>();
50
+
51
+ constructor(ctx: Context, config: Config) {
52
+ super(ctx, config);
53
+ this.defaultMode = ctx.sandboxPolicy.defaultMode;
54
+ this.source = ruleSourceOf(config, process.env);
55
+ }
56
+
57
+ /** 工具层读它判断后端是否 confine(并据此广告 escalation 字段)。 */
58
+ override get sandboxMode(): SandboxMode {
59
+ return this.defaultMode;
60
+ }
61
+
62
+ /**
63
+ * 解析目标后立即执行拒绝判定:工具入口(read / write / edit / list)都先经过
64
+ * `resolve`,因此一次判定即可覆盖读与写。
65
+ * @param path - 待解析的路径。
66
+ * @param opts - cwd 与取消信号;cwd 同时是相对规则的解析根。
67
+ * @returns 解析后的目标。
68
+ */
69
+ override async resolve(
70
+ path: string,
71
+ opts?: { cwd?: string; signal?: AbortSignal },
72
+ ): Promise<FsTarget> {
73
+ const target = await super.resolve(path, opts);
74
+ this.assertNotDenied(
75
+ this.rulesFor(opts?.cwd ?? this.ctx.sandboxPolicy.workspaceRoot),
76
+ target.targetKey,
77
+ target.displayPath,
78
+ );
79
+ return target;
80
+ }
81
+
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
+ override async writeText(
92
+ target: FsTarget,
93
+ content: string,
94
+ expected?: FsWriteIntent,
95
+ signal?: AbortSignal,
96
+ sandboxPolicy?: SandboxExecutionPolicy,
97
+ ): Promise<FsWriteOutcome> {
98
+ return super.writeText(
99
+ await this.checkedTarget(target, sandboxPolicy),
100
+ content,
101
+ expected,
102
+ signal,
103
+ );
104
+ }
105
+
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
+ override async editText(
116
+ target: FsTarget,
117
+ edit: FsEditRequest,
118
+ expected?: { version: FsVersion },
119
+ signal?: AbortSignal,
120
+ sandboxPolicy?: SandboxExecutionPolicy,
121
+ ): Promise<FsEditOutcome> {
122
+ return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
123
+ }
124
+
125
+ /** `--` 条目命中即拒绝访问(读与写都拒);抛 `FS_SANDBOX_DENIED`。 */
126
+ private assertNotDenied(
127
+ rules: CompiledRules,
128
+ canonicalTarget: string,
129
+ displayPath: string,
130
+ ): void {
131
+ if (!isDenied(rules, canonicalTarget)) return;
132
+ throw new FsError(
133
+ `cannot access "${displayPath}": file access denied by the configured "--" rules`,
134
+ "FS_SANDBOX_DENIED",
135
+ );
136
+ }
137
+
138
+ /** `--` 或 `r-` 条目命中即拒绝写入,且优先于任何可写根。 */
139
+ private assertWritable(rules: CompiledRules, canonicalTarget: string, displayPath: string): void {
140
+ this.assertNotDenied(rules, canonicalTarget, displayPath);
141
+ if (!isReadOnly(rules, canonicalTarget)) return;
142
+ throw new FsError(
143
+ `cannot write "${displayPath}": path is read-only by the configured "r-" rule`,
144
+ "FS_SANDBOX_DENIED",
145
+ );
146
+ }
147
+
148
+ /**
149
+ * 写前复核:`--` / `r-` 条目(任何模式)→ 模式本身的只读拒绝 → `workspace-write` 的
150
+ * `writableRoots + rw` containment,返回必须被写入的那个目标。
151
+ */
152
+ private async checkedTarget(
153
+ target: FsTarget,
154
+ sandboxPolicy?: SandboxExecutionPolicy,
155
+ ): Promise<FsTarget> {
156
+ const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
157
+ const rules = this.rulesFor(policy.workspaceRoot);
158
+ this.assertWritable(rules, target.targetKey, target.displayPath);
159
+ const { mode } = policy;
160
+ if (mode === "danger-full-access") return target;
161
+ if (mode === "read-only") {
162
+ throw new FsError(
163
+ `cannot write "${target.displayPath}": file access denied under read-only mode`,
164
+ "FS_SANDBOX_DENIED",
165
+ );
166
+ }
167
+ // workspace-write:在新鲜解析出的 canonical 目标上复核,写入也用它(避免
168
+ // “检查这里、写那里”的 TOCTOU 窗口)。
169
+ const fresh = await super.resolve(target.displayPath);
170
+ this.assertWritable(rules, fresh.targetKey, fresh.displayPath);
171
+ for (const root of [...writableRoots(policy), ...rules.allowRoots]) {
172
+ if (await isPathUnder(fresh.targetKey, root)) return fresh;
173
+ }
174
+ throw new FsError(
175
+ `cannot write "${target.displayPath}": file access denied under workspace-write mode`,
176
+ "FS_SANDBOX_DENIED",
177
+ );
178
+ }
179
+
180
+ /** 取(并按需编译缓存)某个工作区根下的规则。 */
181
+ private rulesFor(workspaceRoot: string): CompiledRules {
182
+ const cached = this.compiled.get(workspaceRoot);
183
+ if (cached !== undefined) return cached;
184
+ const compiled = compileRules(this.source, workspaceRoot);
185
+ this.compiled.set(workspaceRoot, compiled);
186
+ return compiled;
187
+ }
188
+ }
189
+
190
+ export default ConfigurableFileSystem;
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
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
+ import type { Context } from "@deepseek-ai/cordis";
18
+ import type { Config } from "./config.ts";
19
+ import { DIALECT_CAPABILITIES } from "./dialects.ts";
20
+ import { ConfigurableFileSystem } from "./fs.ts";
21
+ import { ruleSourceOf } from "./rules.ts";
22
+ import { ConfigurableSandboxProvider } from "./sandbox.ts";
23
+
24
+ // 值与类型一起转出:Loader 读 `Config`(schema),消费方读类型。
25
+ export { Config } from "./config.ts";
26
+
27
+ /** Cordis 插件名。 */
28
+ export const name = "sandbox-local";
29
+
30
+ /** fs 侧从策略服务取默认模式与工作区回退根,所以先等 `ctx.sandboxPolicy`。 */
31
+ export const inject = ["sandboxPolicy"];
32
+
33
+ /**
34
+ * 规则在进程沙箱侧的能力随平台方言变化,加载期把降级说清楚:
35
+ * `ctx.fs` 侧(read / write / edit 工具)在 macOS / Linux / Windows 上语义一致,
36
+ * 只有 bash 等子进程走平台 runner 的表达能力。
37
+ */
38
+ function warnAboutDegradedRules(ctx: Context, config: Config): void {
39
+ const rules = ruleSourceOf(config, process.env);
40
+ const grants = rules.allowWrite.length > 0;
41
+ const readOnly = rules.readOnly.length > 0;
42
+ const denials = rules.deny.length > 0;
43
+ if (!grants && !readOnly && !denials) return;
44
+ if (process.platform === "darwin") return;
45
+ if (readOnly || denials) {
46
+ const seatbelt = DIALECT_CAPABILITIES.seatbelt.denyReadWrite;
47
+ const bwrap = DIALECT_CAPABILITIES.bwrap.denyWriteOnly;
48
+ ctx.logger.warn(
49
+ `sandbox-local: "r-" / "--" entries cannot be fully enforced for confined subprocesses on ${process.platform} ` +
50
+ `(Seatbelt enforces both; bwrap binds the path read-only, so "--" degrades to write-only: ${bwrap}; ` +
51
+ `Landlock and the Windows ACL runner cannot express a subpath rule at all: ${seatbelt} applies to Seatbelt only) ` +
52
+ "— tools that read through ctx.fs stay covered",
53
+ );
54
+ }
55
+ if (grants && process.platform === "win32") {
56
+ ctx.logger.warn(
57
+ 'sandbox-local: "rw" entries cannot be granted to confined subprocesses on win32; ' +
58
+ "ctx.fs covers the extra roots, the Windows ACL runner does not",
59
+ );
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 注册两个替换实现。
65
+ * @param ctx - 插件上下文(官方 `sandbox` / `fs-sandbox` 行已禁用)。
66
+ * @param config - 已由 schema 填好默认值的配置。
67
+ */
68
+ export function apply(ctx: Context, config: Config): void {
69
+ warnAboutDegradedRules(ctx, config);
70
+ new ConfigurableSandboxProvider(ctx, config);
71
+ new ConfigurableFileSystem(ctx, config);
72
+ }