@stackstackstack/dsh-fs-sandbox 0.1.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/fs/fs-sandbox/README.md
5
+ README.md: 73df31d0f98b602c57a3e86340c3dfe0ee836cd0
6
+ README.zh.md: f4000b200979e951f7e10d452a11773114484185
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # dsh-fs-sandbox — the sandbox-enforcing filesystem backend
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ `SandboxedFileSystem` extends [`LocalFileSystem`](../fs-local/README.md) and registers as `ctx.fs`. It inherits every text-storage mechanic verbatim (resolve, stat, read/stream, list, the atomic write, the read-match-write edit critical section) and adds only a per-call MODE fence on `writeText`/`editText`. Reads always pass through — every mode permits reading.
6
+
7
+ Its plugin config is the local backend config unchanged: `cwd` remains the relative-path resolution default, and `diffBasisMaxBytes` bounds the optional overwrite contextual-diff basis.
8
+
9
+ Loading it INSTEAD OF `dsh-fs-local`, together with a [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/README.md), is the whole swap; the model-facing tools (`dsh-tool-fs`) are untouched. The tool layer resolves the calling session's mode and cwd into the SAME per-call policy bash receives, so the two families never confine to different roots.
10
+
11
+ ## The fence
12
+
13
+ The per-call policy carries the effective mode (session override or escalation grant) together with the calling session's immutable cwd root, falling back to deployment policy only for calls without one:
14
+
15
+ - `read-only` — denies every mutation with the structured `FS_SANDBOX_DENIED`.
16
+ - `workspace-write` — allows a mutation only when the target canonicalizes under a writable root: the workspace root plus the platform temp areas (`/tmp`, `os.tmpdir()`), the SAME set the Seatbelt profile grants, derived from the one [`writableRoots`](../../sandbox/README.md) function so the fs fence and the bash runner cannot drift. Canonical spellings use a lexical fast path; an identity-based ancestor fallback recognizes alias-equivalent roots such as Windows long names and 8.3 names without treating unrelated prefixes as contained. The target is re-canonicalized immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught.
17
+ - `danger-full-access` — delegates unfenced.
18
+
19
+ ## Threat model: a policy fence, not a kernel boundary
20
+
21
+ The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface. This mirrors the `code-runtime` stance: containment, not a security boundary. Kernel-grade isolation of untrusted CODE stays `ctx.shell`'s job ([`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md)). The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs `openat2`-class primitives not worth their portability cost here.
22
+
23
+ A denial is a structured `FsError` (`FS_SANDBOX_DENIED`, carrying the effective mode) — no stderr text inference (unlike bash's kernel denials), because an in-process fence knows exactly what it refused. The model-facing `[sandbox: file access denied under <mode> mode]` marker and the one-approved-wider retry live in the tool layer (`dsh-tool-fs`), exactly as bash's do. See [the cross-family fs sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md).
24
+
25
+ ## Model Experience
26
+
27
+ ### Filesystem policy and refusals
28
+
29
+ #### What the model sees
30
+
31
+ The policy owner contributes capability-neutral `sandbox:policy` context. Indirectly, `dsh-tool-fs` renders this backend's `FS_SANDBOX_DENIED` refusals as the `[sandbox: file access denied under <mode> mode]` marker plus the same-turn escalation hint.
32
+
33
+ #### Token effect
34
+
35
+ The current-policy clause adds a small runtime-context message while this backend is mounted; a denial adds the bounded marker and escalation hint to conversation history.
36
+
37
+ #### KV Cache effect
38
+
39
+ A standing-policy change appends an owner-rendered superseding runtime-context snapshot after retained history; operation results remain append-only.
40
+
41
+ ## Known Limitations and Deferred Work
42
+
43
+ - **A policy fence, not a kernel boundary** — the check is trusted code over a model-controlled path, so the residual resolve-to-syscall TOCTOU is narrowed (by the in-place re-canonicalization) but not eliminated; adversarial host processes are out of scope. Kernel-grade isolation of untrusted code stays `ctx.shell`'s.
44
+ - **Fence-vs-runner parity is derived from one owner** — the writable set comes from `writableRoots`, shared with the Seatbelt profile; a runner profile that defines its writable set elsewhere would drift.
45
+ - **Requires `ctx.sandboxPolicy`** — tools use it to resolve each session policy and the backend uses it for agentless-call fallbacks; the backend does not confine without it composed.
package/README.zh.md ADDED
@@ -0,0 +1,45 @@
1
+ # dsh-fs-sandbox:强制沙箱的文件系统后端
2
+
3
+ [English](README.md) | 中文
4
+
5
+ `SandboxedFileSystem` 扩展 [`LocalFileSystem`](../fs-local/README.md) 并注册为 `ctx.fs`。它逐字继承全部文本存储机制(解析、stat、读取/流式读取、列出、原子写入、按读取、匹配、写入顺序执行的编辑临界区),只为 `writeText`/`editText` 增加按调用的模式围栏。读取始终直接通过:所有模式都允许读取。
6
+
7
+ 它原样复用本地后端配置:`cwd` 仍是相对路径的解析默认值,`diffBasisMaxBytes` 则限制可选的覆写上下文 diff 基础。
8
+
9
+ 只需加载它来替代 `dsh-fs-local`,并同时加载 [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/README.md),即可完成替换;面向模型的工具(`dsh-tool-fs`)无需改动。工具层把调用会话的模式和 cwd 解析为与 bash 相同的按调用策略,因此两个能力族绝不会约束到不同根目录。
10
+
11
+ ## 围栏
12
+
13
+ 按调用策略携带有效模式(会话覆盖值或升级授权)和调用会话不可变的 cwd 根目录;只有没有会话的调用才回退到部署策略:
14
+
15
+ - `read-only`:以结构化 `FS_SANDBOX_DENIED` 拒绝所有变更;
16
+ - `workspace-write`:只有目标规范化后位于可写根目录下,才允许变更。可写根包括工作区根目录和平台临时区域(`/tmp`、`os.tmpdir()`),与 Seatbelt profile 授权的集合相同;该集合由唯一的 [`writableRoots`](../../sandbox/README.md) 函数派生,使 fs 围栏与 bash runner 不会漂移。规范拼写使用词法快速路径;基于身份的祖先回退可以识别 Windows 长名称和 8.3 名称等别名等价根目录,而不会把无关前缀视为包含关系。委托前会立即重新规范化目标,因此工具解析后被替换的祖先符号链接也会被发现;
17
+ - `danger-full-access`:不加围栏直接委托。
18
+
19
+ ## 威胁模型:策略围栏,而非内核边界
20
+
21
+ 围栏是在可信代码中检查模型控制的路径。操作本身属于 seam(open、rename),只有目标路径不可信,因此「规范化后检查包含关系」就是该接口的完整答案。这与 `code-runtime` 的立场相同:提供约束,但不是安全边界。不可信代码的内核级隔离仍由 `ctx.shell` 负责([`dsh-bash-sandbox`](../../shell/bash-sandbox/README.md))。剩余 TOCTOU(在包含关系复查与系统调用之间替换祖先符号链接)会通过写入前立即重新规范化来缩小,并为该威胁模型所接受;内核严密边界需要 `openat2` 一类原语,其可移植性成本在此不值得。
22
+
23
+ 拒绝是结构化 `FsError`(`FS_SANDBOX_DENIED`,携带有效模式),不通过 stderr 文本推断(不同于 bash 的内核拒绝),因为进程内围栏准确知道自己拒绝了什么。面向模型的 `[sandbox: file access denied under <mode> mode]` 标记以及唯一一次获批的更宽权限重试位于工具层(`dsh-tool-fs`),与 bash 完全相同。见[跨能力族 fs 沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)。
24
+
25
+ ## 模型体验
26
+
27
+ ### 文件系统策略与拒绝
28
+
29
+ #### 模型看到的内容
30
+
31
+ 策略归属方会贡献与具体能力无关的 `sandbox:policy` 上下文。作为间接影响,`dsh-tool-fs` 会把本后端的 `FS_SANDBOX_DENIED` 拒绝渲染为 `[sandbox: file access denied under <mode> mode]` 标记和同轮次升级提示。
32
+
33
+ #### Token 影响
34
+
35
+ 该后端挂载期间,当前策略条款会增加一条简短的运行时上下文消息;拒绝则会把有界标记和升级提示追加到对话历史。
36
+
37
+ #### KV Cache 影响
38
+
39
+ 常驻策略发生变化时,会在保留的历史之后追加一份由归属方渲染、取代先前状态的运行时上下文快照;操作结果保持仅追加。
40
+
41
+ ## 已知限制与暂缓事项
42
+
43
+ - **策略围栏,而非内核边界**:该检查是可信代码处理模型控制的路径,因此解析到系统调用之间残留的 TOCTOU 会被原位重新规范化缩小,但不会消除;对抗性宿主进程不在范围内。不可信代码的内核级隔离仍属于 `ctx.shell`。
44
+ - **围栏与 runner 的一致性由单一所有方派生**:可写集合来自 `writableRoots`,该函数与 Seatbelt profile 共享;在其他位置定义可写集合的 runner profile 会发生漂移。
45
+ - **要求 `ctx.sandboxPolicy`**:工具使用它解析每个会话策略,后端用它处理无 agent(智能体)调用的回退;未组合该服务时,后端不会实施约束。
package/lib/index.js ADDED
@@ -0,0 +1,173 @@
1
+ import { LocalFileSystem } from "@stackstackstack/dsh-fs-local";
2
+ import { FsError } from "@stackstackstack/dsh-fs";
3
+ import { writableRoots } from "@stackstackstack/dsh-sandbox";
4
+ import { stat } from "node:fs/promises";
5
+ import { dirname, sep } from "node:path";
6
+ //#region lib/types/containment.js
7
+ /**
8
+ * Path-containment mechanics for the filesystem sandbox. Canonical spellings
9
+ * take the fast lexical path; filesystem identity supplies the conservative
10
+ * fallback for alias-equivalent roots such as Windows 8.3 names and casing.
11
+ * @module @stackstackstack/dsh-fs-sandbox/containment
12
+ */
13
+ const MISSING_CODES = new Set(["ENOENT", "ENOTDIR"]);
14
+ function isMissing(error) {
15
+ const code = error.code;
16
+ return MISSING_CODES.has(code);
17
+ }
18
+ function comparablePath(path, caseSensitive) {
19
+ return caseSensitive ? path : path.toLowerCase();
20
+ }
21
+ function isLexicallyUnder(path, root, caseSensitive) {
22
+ const comparableTarget = comparablePath(path, caseSensitive);
23
+ const comparableRoot = comparablePath(root, caseSensitive);
24
+ if (comparableTarget === comparableRoot) return true;
25
+ const prefix = comparableRoot.endsWith(sep) ? comparableRoot : comparableRoot + sep;
26
+ return comparableTarget.startsWith(prefix);
27
+ }
28
+ async function statIfPresent(path) {
29
+ try {
30
+ return await stat(path, { bigint: true });
31
+ } catch (error) {
32
+ /* v8 ignore else -- a non-missing stat failure requires a host permission or I/O fault after resolve reached this ancestor. */
33
+ if (isMissing(error)) return void 0;
34
+ /* v8 ignore next -- requires a host permission or I/O fault after resolve already reached this ancestor. */
35
+ throw error;
36
+ }
37
+ }
38
+ function sameIdentity(left, right) {
39
+ return left.dev === right.dev && left.ino === right.ino;
40
+ }
41
+ /**
42
+ * Determine whether a canonical target is a writable root or lies beneath it.
43
+ * The lexical fast path handles normal canonical spellings. When spellings
44
+ * differ, walk the target's existing ancestors and compare filesystem identity
45
+ * with the root; this recognizes Windows long-name/8.3 aliases and casing
46
+ * without weakening containment to a textual approximation.
47
+ * @param path - canonical target key, which may end in a missing suffix.
48
+ * @param root - canonical writable root.
49
+ * @param caseSensitive - whether lexical comparison preserves case; defaults
50
+ * to the host filesystem convention used by supported platforms.
51
+ * @returns whether the target is the root or a descendant of it.
52
+ */
53
+ async function isPathUnder(path, root, caseSensitive = process.platform !== "win32") {
54
+ if (isLexicallyUnder(path, root, caseSensitive)) return true;
55
+ const rootInfo = await statIfPresent(root);
56
+ if (!rootInfo) return false;
57
+ let ancestor = path;
58
+ while (true) {
59
+ const ancestorInfo = await statIfPresent(ancestor);
60
+ if (ancestorInfo && sameIdentity(ancestorInfo, rootInfo)) return true;
61
+ const parent = dirname(ancestor);
62
+ if (parent === ancestor) return false;
63
+ ancestor = parent;
64
+ }
65
+ }
66
+ //#endregion
67
+ //#region lib/types/index.js
68
+ /**
69
+ * `SandboxedFileSystem`: the sandbox-enforcing implementation of the
70
+ * `@stackstackstack/dsh-fs` Service Definition. It extends `LocalFileSystem` so all
71
+ * text-storage mechanics — resolve, stat, read/stream, list, the atomic
72
+ * write and the read-match-write edit critical section — are the local
73
+ * implementation's, verbatim; this package adds only the per-call POLICY fence
74
+ * on the two mutations. Reads pass through untouched: every mode permits
75
+ * reading.
76
+ *
77
+ * The fence is a policy check in TRUSTED code over a MODEL-CONTROLLED path,
78
+ * NOT a kernel boundary — the operations are the seam's own (open, rename),
79
+ * and only the target path is untrusted, so canonicalize-then-contain is the
80
+ * complete answer to this surface. Kernel-grade isolation of untrusted CODE
81
+ * stays `ctx.shell`'s job (`@stackstackstack/dsh-bash-sandbox`). This mirrors the
82
+ * `code-runtime` stance: containment, not a security boundary. The residual
83
+ * TOCTOU (an ancestor symlink swapped between the containment re-check and the
84
+ * syscall) is narrowed by re-canonicalizing immediately before delegating and
85
+ * is accepted for this threat model.
86
+ *
87
+ * Per-call policy: `read-only` denies every mutation; `workspace-write` allows
88
+ * a mutation only when the target canonicalizes under the policy's workspace
89
+ * root or a platform temp area (the SAME writable-root set Seatbelt grants,
90
+ * derived from the one `writableRoots` function so bash and fs cannot drift);
91
+ * `danger-full-access` delegates unfenced. A denial throws the structured
92
+ * `FS_SANDBOX_DENIED` — no text inference is needed (unlike bash's kernel
93
+ * stderr), because an in-process fence knows exactly what it refused. The
94
+ * escalation retry lives in the tool layer (`@stackstackstack/dsh-tool-fs`),
95
+ * exactly as bash's does.
96
+ *
97
+ * @module @stackstackstack/dsh-fs-sandbox
98
+ */
99
+ /**
100
+ * Sandbox-enforcing filesystem backend. Registers as `ctx.fs` (loading it
101
+ * INSTEAD OF `dsh-fs-local`, together with a `ctx.sandboxPolicy`, is the whole
102
+ * swap — the model-facing tools are untouched). Its configured default mode is
103
+ * the capability fact exposed by {@link sandboxMode}; `dsh-tool-fs` resolves
104
+ * each session's mode and cwd into a policy for every mutation, while an
105
+ * approved escalation may stamp a strictly wider mode for one call.
106
+ */
107
+ var SandboxedFileSystem = class extends LocalFileSystem {
108
+ static inject = ["sandboxPolicy"];
109
+ defaultMode;
110
+ constructor(ctx, config) {
111
+ super(ctx, config);
112
+ this.defaultMode = ctx.sandboxPolicy.defaultMode;
113
+ }
114
+ /** The deployment default mode — the capability fact the tool layer reads to advertise escalation. */
115
+ get sandboxMode() {
116
+ return this.defaultMode;
117
+ }
118
+ /**
119
+ * Fence the write by the per-call policy, then delegate to the inherited
120
+ * atomic write. See {@link checkedTarget}.
121
+ * @param target - the resolved target to write.
122
+ * @param content - the full new file content.
123
+ * @param expected - the write intent guarding the write; omit for unconditional.
124
+ * @param signal - aborts before atomic publication takes effect.
125
+ * @param sandboxPolicy - the per-call mode and workspace root; omit to use
126
+ * the deployment fallback.
127
+ * @returns the write outcome from the inherited backend.
128
+ */
129
+ async writeText(target, content, expected, signal, sandboxPolicy) {
130
+ return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
131
+ }
132
+ /**
133
+ * Fence the edit by the per-call policy, then delegate to the inherited
134
+ * atomic edit. See {@link checkedTarget}.
135
+ * @param target - the resolved target to edit.
136
+ * @param edit - the literal search/replace request.
137
+ * @param expected - the version guard; omit for an unconditional edit.
138
+ * @param signal - aborts before atomic publication takes effect.
139
+ * @param sandboxPolicy - the per-call mode and workspace root; omit to use
140
+ * the deployment fallback.
141
+ * @returns the edit outcome from the inherited backend.
142
+ */
143
+ async editText(target, edit, expected, signal, sandboxPolicy) {
144
+ return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
145
+ }
146
+ /**
147
+ * Enforce the per-call policy against `target` and return the EXACT target the
148
+ * mutation must use, so the checked identity is the mutated one (no
149
+ * check-here-write-there TOCTOU). `read-only` denies; `workspace-write`
150
+ * re-canonicalizes NOW (`resolve` realpaths the deepest existing ancestor,
151
+ * reflecting a concurrently swapped symlink), requires containment under a
152
+ * writable root, and returns THAT fresh target; `danger-full-access` returns
153
+ * the caller's target unfenced. Throws the structured `FS_SANDBOX_DENIED` on
154
+ * refusal — the tool layer maps it to the model-facing `[sandbox: …]` marker
155
+ * and the escalation hint.
156
+ */
157
+ async checkedTarget(target, sandboxPolicy) {
158
+ const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
159
+ const { mode } = policy;
160
+ if (mode === "danger-full-access") return target;
161
+ if (mode === "read-only") throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
162
+ const fresh = await this.resolve(target.displayPath);
163
+ let contained = false;
164
+ for (const root of writableRoots(policy)) if (await isPathUnder(fresh.targetKey, root)) {
165
+ contained = true;
166
+ break;
167
+ }
168
+ if (!contained) throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, "FS_SANDBOX_DENIED");
169
+ return fresh;
170
+ }
171
+ };
172
+ //#endregion
173
+ export { SandboxedFileSystem, SandboxedFileSystem as default };
@@ -0,0 +1,20 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@stackstackstack/dsh-fs-sandbox`.
4
+ * @module @stackstackstack/dsh-fs-sandbox/invariant
5
+ */
6
+ const PACKAGE_NAME = "@stackstackstack/dsh-fs-sandbox";
7
+ /** Cordis companion plugin name. */
8
+ const name = "fs-sandbox-invariant";
9
+ /** Services required before the companion can register. */
10
+ const inject = ["invariants"];
11
+ /** No runtime invariant: this stateless adapter delegates policy and filesystem relations to their owning seams. */
12
+ const install = () => {};
13
+ /**
14
+ * Register this package's invariant companion.
15
+ * @param ctx - Cordis context carrying the invariant service.
16
+ * @returns the installed registration's disposer after setup succeeds.
17
+ */
18
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
19
+ //#endregion
20
+ export { apply, inject, name };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Path-containment mechanics for the filesystem sandbox. Canonical spellings
3
+ * take the fast lexical path; filesystem identity supplies the conservative
4
+ * fallback for alias-equivalent roots such as Windows 8.3 names and casing.
5
+ * @module @stackstackstack/dsh-fs-sandbox/containment
6
+ */
7
+ /**
8
+ * Determine whether a canonical target is a writable root or lies beneath it.
9
+ * The lexical fast path handles normal canonical spellings. When spellings
10
+ * differ, walk the target's existing ancestors and compare filesystem identity
11
+ * with the root; this recognizes Windows long-name/8.3 aliases and casing
12
+ * without weakening containment to a textual approximation.
13
+ * @param path - canonical target key, which may end in a missing suffix.
14
+ * @param root - canonical writable root.
15
+ * @param caseSensitive - whether lexical comparison preserves case; defaults
16
+ * to the host filesystem convention used by supported platforms.
17
+ * @returns whether the target is the root or a descendant of it.
18
+ */
19
+ export declare function isPathUnder(path: string, root: string, caseSensitive?: boolean): Promise<boolean>;
20
+ //# sourceMappingURL=containment.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `SandboxedFileSystem`: the sandbox-enforcing implementation of the
3
+ * `@stackstackstack/dsh-fs` Service Definition. It extends `LocalFileSystem` so all
4
+ * text-storage mechanics — resolve, stat, read/stream, list, the atomic
5
+ * write and the read-match-write edit critical section — are the local
6
+ * implementation's, verbatim; this package adds only the per-call POLICY fence
7
+ * on the two mutations. Reads pass through untouched: every mode permits
8
+ * reading.
9
+ *
10
+ * The fence is a policy check in TRUSTED code over a MODEL-CONTROLLED path,
11
+ * NOT a kernel boundary — the operations are the seam's own (open, rename),
12
+ * and only the target path is untrusted, so canonicalize-then-contain is the
13
+ * complete answer to this surface. Kernel-grade isolation of untrusted CODE
14
+ * stays `ctx.shell`'s job (`@stackstackstack/dsh-bash-sandbox`). This mirrors the
15
+ * `code-runtime` stance: containment, not a security boundary. The residual
16
+ * TOCTOU (an ancestor symlink swapped between the containment re-check and the
17
+ * syscall) is narrowed by re-canonicalizing immediately before delegating and
18
+ * is accepted for this threat model.
19
+ *
20
+ * Per-call policy: `read-only` denies every mutation; `workspace-write` allows
21
+ * a mutation only when the target canonicalizes under the policy's workspace
22
+ * root or a platform temp area (the SAME writable-root set Seatbelt grants,
23
+ * derived from the one `writableRoots` function so bash and fs cannot drift);
24
+ * `danger-full-access` delegates unfenced. A denial throws the structured
25
+ * `FS_SANDBOX_DENIED` — no text inference is needed (unlike bash's kernel
26
+ * stderr), because an in-process fence knows exactly what it refused. The
27
+ * escalation retry lives in the tool layer (`@stackstackstack/dsh-tool-fs`),
28
+ * exactly as bash's does.
29
+ *
30
+ * @module @stackstackstack/dsh-fs-sandbox
31
+ */
32
+ import { Context } from '@deepseek-ai/cordis';
33
+ import { LocalFileSystem } from '@stackstackstack/dsh-fs-local';
34
+ import type { Config as LocalConfig } from '@stackstackstack/dsh-fs-local';
35
+ import type { FsEditOutcome, FsEditRequest, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from '@stackstackstack/dsh-fs';
36
+ import type { SandboxExecutionPolicy, SandboxMode } from '@stackstackstack/dsh-sandbox';
37
+ /**
38
+ * Plugin config: the local backend's knobs verbatim (`cwd` resolution default
39
+ * and `diffBasisMaxBytes` overwrite-presentation bound). The sandbox default
40
+ * (mode + `workspace-write` fallback root) is NOT here — `ctx.sandboxPolicy`
41
+ * resolves each calling session for every enforcing capability.
42
+ */
43
+ export type Config = LocalConfig;
44
+ /**
45
+ * Sandbox-enforcing filesystem backend. Registers as `ctx.fs` (loading it
46
+ * INSTEAD OF `dsh-fs-local`, together with a `ctx.sandboxPolicy`, is the whole
47
+ * swap — the model-facing tools are untouched). Its configured default mode is
48
+ * the capability fact exposed by {@link sandboxMode}; `dsh-tool-fs` resolves
49
+ * each session's mode and cwd into a policy for every mutation, while an
50
+ * approved escalation may stamp a strictly wider mode for one call.
51
+ */
52
+ export declare class SandboxedFileSystem extends LocalFileSystem {
53
+ static inject: string[];
54
+ private readonly defaultMode;
55
+ constructor(ctx: Context, config: Config);
56
+ /** The deployment default mode — the capability fact the tool layer reads to advertise escalation. */
57
+ get sandboxMode(): SandboxMode;
58
+ /**
59
+ * Fence the write by the per-call policy, then delegate to the inherited
60
+ * atomic write. See {@link checkedTarget}.
61
+ * @param target - the resolved target to write.
62
+ * @param content - the full new file content.
63
+ * @param expected - the write intent guarding the write; omit for unconditional.
64
+ * @param signal - aborts before atomic publication takes effect.
65
+ * @param sandboxPolicy - the per-call mode and workspace root; omit to use
66
+ * the deployment fallback.
67
+ * @returns the write outcome from the inherited backend.
68
+ */
69
+ writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsWriteOutcome>;
70
+ /**
71
+ * Fence the edit by the per-call policy, then delegate to the inherited
72
+ * atomic edit. See {@link checkedTarget}.
73
+ * @param target - the resolved target to edit.
74
+ * @param edit - the literal search/replace request.
75
+ * @param expected - the version guard; omit for an unconditional edit.
76
+ * @param signal - aborts before atomic publication takes effect.
77
+ * @param sandboxPolicy - the per-call mode and workspace root; omit to use
78
+ * the deployment fallback.
79
+ * @returns the edit outcome from the inherited backend.
80
+ */
81
+ editText(target: FsTarget, edit: FsEditRequest, expected?: {
82
+ version: FsVersion;
83
+ }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
84
+ /**
85
+ * Enforce the per-call policy against `target` and return the EXACT target the
86
+ * mutation must use, so the checked identity is the mutated one (no
87
+ * check-here-write-there TOCTOU). `read-only` denies; `workspace-write`
88
+ * re-canonicalizes NOW (`resolve` realpaths the deepest existing ancestor,
89
+ * reflecting a concurrently swapped symlink), requires containment under a
90
+ * writable root, and returns THAT fresh target; `danger-full-access` returns
91
+ * the caller's target unfenced. Throws the structured `FS_SANDBOX_DENIED` on
92
+ * refusal — the tool layer maps it to the model-facing `[sandbox: …]` marker
93
+ * and the escalation hint.
94
+ */
95
+ private checkedTarget;
96
+ }
97
+ export default SandboxedFileSystem;
98
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@stackstackstack/dsh-fs-sandbox`.
3
+ * @module @stackstackstack/dsh-fs-sandbox/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "fs-sandbox-invariant";
8
+ /** Services required before the companion can register. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@stackstackstack/dsh-fs-sandbox",
3
+ "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through",
4
+ "version": "0.1.5",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/fs/fs-sandbox"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "@stackstackstack/dsh-fs": "^0.1.5",
36
+ "@stackstackstack/dsh-fs-local": "^0.1.5",
37
+ "@stackstackstack/dsh-invariants": "^0.1.5",
38
+ "@stackstackstack/dsh-sandbox": "^0.1.5",
39
+ "@stackstackstack/dsh-sandbox-policy": "^0.1.5",
40
+ "@deepseek-ai/cordis": "^4.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@stackstackstack/dsh-fs-local": "^0.1.5",
44
+ "@stackstackstack/dsh-fs": "^0.1.5",
45
+ "@stackstackstack/dsh-sandbox-policy": "^0.1.5",
46
+ "@stackstackstack/dsh-invariants": "^0.1.5",
47
+ "@deepseek-ai/cordis": "^4.0.1",
48
+ "@stackstackstack/dsh-sandbox": "^0.1.5"
49
+ }
50
+ }