@deepseek-ai/dsh-fs-local 0.1.1-rc.1 → 0.1.2-alpha.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.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/fs/fs-local/README.md
5
- README.md: 4d5c42945b86ccdc8b041d9d7f99a067ab9a37f5
6
- README.zh.md: 51f417f73294b7497563fece168e5a5d44b73421
5
+ README.md: a8a4815bf26985f3d34d1a11e69272f5e429e1e3
6
+ README.zh.md: d0a9f357a98ea7c989a92ce3d40ed240fd316b00
package/README.md CHANGED
@@ -1,33 +1,119 @@
1
+ ---
2
+ description: "The host-filesystem backend for ctx.fs for deployments and maintainers choosing or debugging local file access."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-fs-local
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The **local-filesystem implementation** of the `ctx.fs` provider contract ([`@deepseek-ai/dsh-fs`](../fs)). Backs the twelve `FileSystem` primitives with the host filesystem; loading it as a plugin populates `ctx.fs`.
10
+ ## Summary
11
+
12
+ `dsh-fs-local` implements the `ctx.fs` filesystem contract ([`dsh-fs`](../fs/README.md)) on the host filesystem: loading it as a plugin populates `ctx.fs` with real file access — resolve, read, list, atomic write, and literal edit against the local machine's files. Relative paths resolve from a configurable base directory, and the same file reached through different paths or symlinks shares one identity. Because this backend shares the host filesystem, it can also map an absolute host path into the process path used by this execution world. Writes are atomic and preserve file permissions; the optional version guard makes stale overwrites fail instead of clobbering. Choose it when a process needs direct, unconfined access to host files; choose `fs-sandbox` when mutations must be confined, or `fs-e2b` when file state belongs in a remote execution world.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount this backend when a composition needs `ctx.fs` backed by the real host filesystem and accepts a process-local implementation. The common path is explicit: load the backend, give it a base directory, and the model-facing tools (`dsh-tool-fs`) or your own plugins can read, write, and edit files.
29
+
30
+ ### When to choose it
31
+
32
+ Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence. Choose [`fs-e2b`](../../e2b/fs-e2b/README.md) when files must live in a remote execution world shared with subprocesses. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it.
33
+
34
+ ### Minimal configuration
6
35
 
7
- ```ts ignore-check
8
- import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
36
+ Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it.
9
37
 
10
- await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
11
- // ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-observation-policy for the
12
- // freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-fs-local'
40
+ config:
41
+ cwd: /absolute/path/to/workspace
13
42
  ```
14
43
 
15
- ## Behavior
44
+ | Field | Default | Meaning |
45
+ |---|---|---|
46
+ | `cwd` | `process.cwd()` | Base directory for relative paths |
47
+ | `diffBasisMaxBytes` | `10 MiB` | UTF-8 byte limit per overwrite-diff side; larger overwrites return `before: null` |
48
+
49
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-fs-local) is the exhaustive source for every accepted field and its JSDoc.
50
+
51
+ ### What you can do
52
+
53
+ Read any regular UTF-8 text file whole or as a stream, read raw bytes up to a cap you choose, and list one directory level in stable name order. Create or replace a file atomically, and apply a literal text edit atomically; both mutations serialize per file, so concurrent writers never interleave. The version guard is optional: omit it for unconditional create-or-overwrite, or supply it to fail when the file changed since you last observed it.
54
+
55
+ Failures are typed `FsError`s with stable codes — `FS_NOT_FOUND`, `FS_NOT_TEXT` (binary content), `FS_STALE_VERSION` (changed since observation), `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT` (no unique literal match), and others — so callers branch on the code, never on message text. A missing target on a guarded edit reports `FS_STALE_VERSION` either way.
56
+
57
+ -----
58
+
59
+ <a id="understand-the-implementation"></a>
60
+ ## Understand the implementation
61
+
62
+ <details>
63
+ <summary>Implementation internals — click to expand</summary>
64
+
65
+ This section explains the design decisions behind the local backend and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
66
+
67
+ ### Design concept
68
+
69
+ The backend builds on three ideas:
16
70
 
17
- - **`resolve(path, opts?)`** — a relative `path` resolves against `opts.cwd` when the caller supplies one (the model-facing tools pass the calling agent's session cwd — see [the per-session cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)), else `config.cwd` (default `process.cwd()`); an absolute `path` ignores both. `opts.signal` is checked before and after local resolution, while a remote sibling backend may use it to abort its round-trip. The `targetKey` is the file's `realpath`, so two input paths reaching the same file through symlinks share one identity, and writes/edits land on the link target (preserving the link). A not-yet-existing path uses the realpathed parent directory plus basename when the parent exists; only an unresolvable parent falls back to the absolute path. `displayPath` is the absolute (un-resolved) path.
18
- - **Execution-world coordinates** — `processPath` exposes the target's canonical host path, `fileUrl` encodes that path through Node's platform-aware URL conversion, and `contains` uses platform path semantics to test identity or descendant containment without consumers parsing `targetKey`.
19
- - **`stat` / `lstat`** — return target metadata or `undefined` when absent. `stat` reports `FsInfo` for an already resolved target (`version` = an opaque token derived from bigint `dev:ino:size:mtimeNs:ctimeNs`, `type` of `file`/`directory`/`other`, byte `size`); path-shaped `lstat` reports `FsPathInfo` without following the final symlink and can therefore return `symlink`. Both check cancellation before and after their asynchronous metadata probe, so an abort that lands in flight reports `FS_ABORTED` rather than stale absence.
20
- - **`readText` / `streamText`** — UTF-8 only. `readText` reads the whole file; `streamText` decodes chunks so a huge file need not be held whole in memory and consumers can enforce their own retention bounds. Both reject invalid UTF-8 and NUL-byte binary samples (`FS_NOT_TEXT`) and non-regular targets. The `read` tool (`@deepseek-ai/dsh-tool-fs`) owns line windowing.
21
- - **`readBytes`** — raw whole-file bytes with no decoding or binary rejection (the `read_image` tool validates content through the attachment service). The required byte cap short-circuits on the stat size before any content I/O; the subsequent stream reads at most one byte beyond the cap, so a file growing after stat still fails `FS_TOO_LARGE` without unbounded buffering.
22
- - **`listDir`** — lists one directory level in stable `name.localeCompare()` order. Each entry carries the child basename, type, resolved child target (`displayPath` under the listed directory, `targetKey` as the realpath identity), and cheap stat metadata (`version`, plus `size` for regular files). It never opens or decodes file contents. Missing targets report `FS_NOT_FOUND`, file/special-file targets report `FS_NOT_DIRECTORY`, aborted calls report `FS_ABORTED`, permission failures report `FS_PERMISSION_DENIED`, and other listing or child metadata I/O failures report `FS_IO_ERROR`. Broken/disappeared children are returned as `other` without metadata, but permission/IO failures while resolving a child fail the whole listing with a structured `FsError`.
23
- - **`writeText`** — atomic: writes to a temp file opened exclusively (`wx`, `0o600`) inside a randomly-named private staging dir (`0o700`) next to the target, then fsyncs and publishes. An existing file's mode is preserved, while new files default to `0o600`; on Windows a new file inherits the destination directory's DACL, while replacement copies the target DACL onto the empty temp before writing and publishes through `ReplaceFileW` so the original access policy survives ([Windows DACL preservation Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md)). The `expected` guard is OPTIONAL: omitting it unconditionally creates-or-overwrites; `createIfAbsent` hard-links the staged file into place as an atomic no-replace publication, so a regular file created after the initial probe is preserved and rejected with `FS_NOT_OBSERVED`, while a non-regular path entry is preserved and rejected with `FS_NOT_REGULAR_FILE`; `replaceIfVersion` replaces only at the observed version (a missing target or mismatch is `FS_STALE_VERSION`). An overwrite returns the prior text as its contextual diff basis only when both the opened prior file and UTF-8 replacement are strictly below `config.diffBasisMaxBytes` (default 10 MiB). The descriptor read enforces that limit even if an external writer replaces or changes the file size after the initial probe. Otherwise the provider returns `before: null`, so presentation uses its whole-file fallback.
24
- - **`editText`** — atomic literal read-modify-write over the same primitive, serialized per target by a mutation lock. The `expected` guard is OPTIONAL: when supplied it verifies the version BEFORE literal matching (a stale edit reports `FS_STALE_VERSION`, never `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT` against newer content); omitting it edits the current content unconditionally. A missing target reports `FS_STALE_VERSION` either way. LF-normalizes for matching, restores the file's dominant CRLF/LF style, and rejects empty `oldString` / zero matches (`FS_EDIT_NOT_FOUND`) or ambiguous multi-matches without `replace_all` (`FS_AMBIGUOUS_EDIT`).
71
+ - **Realpath identity.** The `targetKey` is the file's `realpath`, so two input paths reaching the same file through symlinks share one identity, and writes land on the link target while preserving the link.
72
+ - **Atomic publication.** Writes stage into an exclusive temp file inside a private staging directory next to the target, fsync, then publish; an existing file's mode is preserved and Windows DACLs survive replacement.
73
+ - **One mutation critical section.** A per-target FIFO lock serializes read→guard→write windows, so concurrent writes and edits are deterministically ordered — one wins, the rest see the new version and reject as stale.
25
74
 
26
- The package-root SDK API is the default/named `LocalFileSystem` class plus `Config`. Raw I/O lives in `src/fsio.ts` (Cordis-free, independently unit-tested); `src/index.ts` is the thin service wiring.
75
+ ### Source map
27
76
 
77
+ | File | Role |
78
+ |---|---|
79
+ | [`src/index.ts`](src/index.ts) | Service wiring: `LocalFileSystem`, `Config`, per-target mutation lock |
80
+ | [`src/fsio.ts`](src/fsio.ts) | Cordis-free raw I/O: probe, reads, atomic write, literal edit, line-ending handling |
81
+ | [`src/win32.ts`](src/win32.ts) | Windows-specific DACL preservation for atomic replacement |
82
+
83
+ ### Write path
84
+
85
+ Each write probes the target, enforces the optional guard (`createIfAbsent` or `replaceIfVersion`), captures a bounded `before` diff basis when both sides are small enough, stages the new content next to the target, fsyncs, and publishes atomically. Guarded creation uses a hard-link publication that never replaces a concurrent creator, rejecting it with `FS_NOT_OBSERVED` instead.
86
+
87
+ ### Edit path
88
+
89
+ Each edit probes, verifies the version guard before literal matching (so stale edits report `FS_STALE_VERSION`, never a misleading no-match), reads the file, applies the literal replacement with LF normalization, restores the file's dominant line-ending style, and republishes — all inside the per-target lock.
90
+
91
+ ### Ownership and invariants
92
+
93
+ Raw I/O is Cordis-free and independently unit-tested in `src/fsio.ts`; `src/index.ts` stays thin wiring. `config.cwd` is a resolution default only — containment is the job of `fs-sandbox` or a `tools/execute` permission plugin. Cancellation is a best-effort `AbortSignal` checked before and after each asynchronous probe.
94
+
95
+ </details>
96
+
97
+ -----
98
+
99
+ <a id="further-exploration"></a>
100
+ ## Further Exploration
101
+
102
+ Read these pages when the package-level contract is not enough. They move from the contract to the adjacent backends, tools, and policies.
103
+
104
+ - [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy.
105
+ - [dsh-fs](../fs/README.md) — the `ctx.fs` contract this backend implements.
106
+ - [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend that extends this one.
107
+ - [tool-fs](../tool-fs/README.md) — the model-facing tools that consume `ctx.fs`.
108
+ - [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events.
109
+ - [Windows DACL preservation note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md) — why atomic replacement copies the target's access policy.
110
+
111
+ -----
112
+
113
+ <a id="model-experience"></a>
28
114
  ## Model Experience
29
115
 
30
- Indirectly, through [`dsh-tool-fs`](../tool-fs/README.md), which renders this provider's line-windowed UTF-8 content, mutation acknowledgements, and exact provider messages in capped retained results while versions, atomic-write mechanics, and directory metadata remain internal.
116
+ Indirectly, through `dsh-tool-fs`, which renders this provider's line-windowed UTF-8 content, mutation acknowledgements, and exact provider messages in capped retained results while versions, atomic-write mechanics, and directory metadata remain internal.
31
117
 
32
118
  #### KV Cache effect
33
119
 
@@ -35,11 +121,26 @@ No direct invalidation; the named consumer owns any request-prefix changes.
35
121
 
36
122
  ## Known Limitations and Deferred Work
37
123
 
38
- - **`config.cwd` is not a sandbox** — it is a resolution default, not containment: absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall ([capability-seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences)).
124
+ <a id="known-limitations-and-deferred-work"></a>
125
+
126
+
127
+ These limits define when the local backend is a poor fit or needs special operational care. They are current package constraints, not a general filesystem comparison or a task backlog.
128
+
129
+ - **`config.cwd` is not a sandbox** — it is a resolution default, not containment: absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall ([capability-seam note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md)).
39
130
  - **Version tokens depend on filesystem metadata** — they combine device, inode, size, nanosecond mtime, and nanosecond ctime; a storage layer that cannot update any of those facts for a rewrite can still defeat the stale guard.
40
131
  - **`editText` holds the whole file (plus the edited copy) in memory** — streaming exists only on the read path.
41
- - **A sub-limit overwrite still buffers a contextual basis** — `writeText` may retain up to just below `config.diffBasisMaxBytes` of prior text in addition to the caller-owned replacement; the bound does not cap the returned `after` value or presentation's whole-file fallback.
132
+ - **A sub-limit overwrite still buffers a contextual basis** — `writeText` may retain up to just below `config.diffBasisMaxBytes` of prior text in addition to the caller-owned replacement; the bound does not cap the returned `after` value or the whole-file presentation fallback.
42
133
  - **Binary detection is asymmetric** — reads NUL-sample only the first 8192 bytes while edits scan the whole buffer, so a file with a late NUL reads fine but rejects edits.
43
- - **The per-target mutation lock is in-process only** — guarded create still uses an atomic no-replace publication across processes, but replacement writers in another process are caught only when the optional version guard observes their metadata change; they are never serialized.
44
- - **Guarded creation requires hard-link support** — filesystems or mounts that reject hard-link publication cannot serve `createIfAbsent`; the provider preserves the missing target and reports `FS_IO_ERROR`.
134
+ - **The per-target mutation lock is in-process only** — guarded creation still uses an atomic no-replace publication across processes, but replacement writers in another process are caught only when the optional version guard observes their metadata change; they are never serialized.
135
+ - **Guarded creation requires hard-link support** — filesystems or mounts that reject hard-link publication cannot serve `createIfAbsent`; the backend preserves the missing target and reports `FS_IO_ERROR`.
45
136
  - **Post-commit cleanup is best effort** — a successful publication remains successful if removal of its owner-only staging directory fails, leaving private residue for later operator cleanup.
137
+
138
+ <a id="dev-note"></a>
139
+ ### Dev Note
140
+
141
+ <details>
142
+ <summary>Working context for maintainers — click to expand</summary>
143
+
144
+ None.
145
+
146
+ </details>
package/README.zh.md CHANGED
@@ -1,33 +1,119 @@
1
+ ---
2
+ description: "`ctx.fs` 的宿主文件系统后端:面向选择或排查本地文件访问的部署方与维护者。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-fs-local
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- `ctx.fs` 提供方约定([`@deepseek-ai/dsh-fs`](../fs))的**本地文件系统实现**。它使用宿主文件系统支持十二个 `FileSystem` 原语;将其作为插件加载会填充 `ctx.fs`。
10
+ ## 概述
11
+
12
+ `dsh-fs-local` 在宿主文件系统上实现 `ctx.fs` 文件系统约定([`dsh-fs`](../fs/README.zh.md)):把它作为插件加载后,`ctx.fs` 就拥有真实的文件访问能力——针对本机文件的解析、读取、列出、原子写入与字面量编辑。相对路径从可配置的基准目录解析,经不同路径或符号链接到达的同一文件共享一个身份。由于本后端共享宿主文件系统,它还可以把绝对宿主路径映射为此执行世界使用的进程路径。写入是原子的并保留文件权限;可选版本防护让陈旧覆盖失败而不是静默覆盖。当进程需要直接、不受约束地访问宿主文件时选择它;需要约束变更时选择 `fs-sandbox`,文件状态属于远程执行世界时选择 `fs-e2b`。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当组合需要由真实宿主文件系统支撑的 `ctx.fs`、且可以接受进程本地实现时,挂载此后端。常用路径是显式的:加载后端、给出基准目录,然后面向模型的工具(`dsh-tool-fs`)或你自己的插件即可读取、写入和编辑文件。
29
+
30
+ ### 何时选择
31
+
32
+ 普通宿主文件访问请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。文件必须位于与子进程共享的远程执行世界时,选择 [`fs-e2b`](../../e2b/fs-e2b/README.zh.md)。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
33
+
34
+ ### 最小配置
6
35
 
7
- ```ts ignore-check
8
- import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
36
+ 加载后端并给出基准目录;相对路径以它为基准解析,绝对路径忽略它。
9
37
 
10
- await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
11
- // ctx.fs uses the local backend; load @deepseek-ai/dsh-fs-observation-policy for the
12
- // freshness policy gate and @deepseek-ai/dsh-tool-fs to expose read/write/edit.
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-fs-local'
40
+ config:
41
+ cwd: /absolute/path/to/workspace
13
42
  ```
14
43
 
15
- ## 行为
44
+ | 字段 | 默认值 | 含义 |
45
+ |---|---|---|
46
+ | `cwd` | `process.cwd()` | 相对路径的基准目录 |
47
+ | `diffBasisMaxBytes` | `10 MiB` | 每次覆写 diff 一侧的 UTF-8 字节上限;更大的覆写返回 `before: null` |
48
+
49
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-fs-local)是每个受支持字段及其 JSDoc 的穷尽式真源。
50
+
51
+ ### 你能做什么
52
+
53
+ 完整或流式读取任意普通 UTF-8 文本文件,按你选择的上限读取原始字节,并按稳定名称顺序列出一层目录。原子地创建或替换文件,并原子地应用字面量文本编辑;两个变更操作都按文件串行化,并发写入方绝不会交错。版本防护是可选的:省略它即无条件创建或覆盖,提供它则在文件自上次观察以来发生变化时失败。
54
+
55
+ 失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。带防护的编辑遇到缺失目标时,无论哪种情况都报告 `FS_STALE_VERSION`。
56
+
57
+ -----
58
+
59
+ <a id="understand-the-implementation"></a>
60
+ ## 理解实现
61
+
62
+ <details>
63
+ <summary>实现细节——点击展开</summary>
64
+
65
+ 本节解释本地后端背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
66
+
67
+ ### 设计理念
68
+
69
+ 后端建立在三个想法之上:
16
70
 
17
- - **`resolve(path, opts?)`**:相对 `path` 在调用方提供 `opts.cwd` 时以该值为基准解析(面向模型的工具会传入调用 agent(智能体)的会话 cwd;见[每会话 cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md)),否则以 `config.cwd` 为基准(默认 `process.cwd()`);绝对 `path` 会忽略两者。`opts.signal` 会在本地解析前后检查,远程同级后端则可以用它中止往返。`targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径会共享一个身份,写入/编辑落在链接目标上,同时保留链接。尚不存在的路径在父目录存在时使用 realpath 后的父目录加 basename;只有父目录无法解析时才回退到绝对路径。`displayPath` 是绝对但未经解析的路径。
18
- - **执行世界坐标**:`processPath` 公开目标的规范化宿主路径,`fileUrl` 通过 Node 的平台感知 URL 转换对该路径编码,`contains` 则使用平台路径语义检查身份相等或后代包含关系,消费方无需解析 `targetKey`。
19
- - **`stat` / `lstat`**:返回目标元数据;目标不存在时返回 `undefined`。`stat` 为已解析目标报告 `FsInfo`(`version` 是由 bigint `dev:ino:size:mtimeNs:ctimeNs` 派生的不透明 token,`type` 为 `file`/`directory`/`other`,`size` 以字节计);路径形态的 `lstat` 不跟随最后一个符号链接,报告 `FsPathInfo`,因此可以返回 `symlink`。两者都会在异步元数据探测前后检查取消,因此异步探测进行期间发生的中止会报告 `FS_ABORTED`,而非陈旧的不存在结果。
20
- - **`readText` / `streamText`**:只支持 UTF-8。`readText` 读取整个文件;`streamText` 按分片解码,因此超大文件无需整体保存在内存中,消费方也可以自行限制保留量。两者都会拒绝无效 UTF-8、包含 NUL 字节的二进制样本(`FS_NOT_TEXT`)以及非普通文件目标。`read` 工具(`@deepseek-ai/dsh-tool-fs`)拥有行窗口逻辑。
21
- - **`readBytes`**:按原始字节读取整个文件,不做解码或二进制拒绝(`read_image` 工具通过附件服务校验内容)。必填的字节上限在任何内容 I/O 之前先按 stat 大小短路;随后的流最多多读一个字节,因此 stat 之后增长的文件仍会以 `FS_TOO_LARGE` 失败,不会无界缓冲。
22
- - **`listDir`**:按稳定的 `name.localeCompare()` 顺序列出一层目录。每个条目携带子项 basename、类型、解析后的子目标(`displayPath` 位于所列目录下,`targetKey` 是 realpath 身份)和低成本 stat 元数据(`version`,普通文件另有 `size`)。它绝不会打开或解码文件内容。缺失目标报告 `FS_NOT_FOUND`,文件/特殊文件目标报告 `FS_NOT_DIRECTORY`,已中止调用报告 `FS_ABORTED`,权限失败报告 `FS_PERMISSION_DENIED`,其他列出或子项元数据 I/O 失败报告 `FS_IO_ERROR`。损坏/消失的子项以无元数据的 `other` 返回,但解析子项时出现权限/I/O 失败会让整个列表以结构化 `FsError` 失败。
23
- - **`writeText`**:原子写入。它会向排他打开的临时文件(`wx`、`0o600`)写入;该文件位于目标旁随机命名的私有暂存目录(`0o700`)内,随后执行 fsync 并发布。现有文件的 mode 会保留,新文件默认为 `0o600`;Windows 上的新文件继承目标目录的 DACL,而替换会在写入前把目标 DACL 复制到空临时文件,并通过 `ReplaceFileW` 发布,使原访问策略得以保留(见 [Windows DACL 保留 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md))。`expected` 防护是可选的:省略时无条件创建或覆盖;`createIfAbsent` 通过硬链接把暂存文件发布到目标位置,以实现原子且不替换的发布,因此初始探测后创建的普通文件会被保留,并以 `FS_NOT_OBSERVED` 拒绝本次写入;非普通路径条目也会被保留,并以 `FS_NOT_REGULAR_FILE` 拒绝;`replaceIfVersion` 只在观察到的版本上替换(目标缺失或版本不匹配均为 `FS_STALE_VERSION`)。仅当打开后的旧文件和 UTF-8 替换内容都严格低于 `config.diffBasisMaxBytes`(默认 10 MiB)时,覆写才返回旧文本作为上下文 diff 基础。即使外部写入方在初次探测后替换文件或改变文件大小,文件描述符读取仍会强制执行该上限;否则提供方返回 `before: null`,由展示层使用整文件回退。
24
- - **`editText`**:在同一原语之上依次执行原子的字面量读取、修改和写入,并通过变更锁按目标串行化。`expected` 防护是可选的:提供时,会在字面量匹配之前校验版本(陈旧编辑报告 `FS_STALE_VERSION`,绝不会针对较新内容报告 `FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT`);省略时,无条件编辑当前内容。无论哪种情况,目标缺失都报告 `FS_STALE_VERSION`。匹配时规范化为 LF,随后恢复文件主要的 CRLF/LF 风格;空 `oldString` / 零匹配报告 `FS_EDIT_NOT_FOUND`,未设置 `replace_all` 的多个匹配则报告 `FS_AMBIGUOUS_EDIT`。
71
+ - **Realpath 身份。** `targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径共享一个身份,写入落在链接目标上,同时保留链接。
72
+ - **原子发布。** 写入先写入目标旁私有暂存目录内的独占临时文件,执行 fsync 后发布;现有文件的 mode 会保留,Windows 上的 DACL 也会在替换后存活。
73
+ - **单一变更临界区。** 每目标 FIFO 锁串行化读取→防护→写入窗口,并发写入与编辑因此被确定性排序——一方胜出,其余看到新版本并以陈旧拒绝。
25
74
 
26
- 包根 SDK 接口包含默认/具名 `LocalFileSystem` 类和 `Config`。原始 I/O 位于 `src/fsio.ts`(不依赖 Cordis,单独进行单元测试);`src/index.ts` 是轻量服务接线。
75
+ ### 源码地图
27
76
 
77
+ | 文件 | 职责 |
78
+ |---|---|
79
+ | [`src/index.ts`](src/index.ts) | 服务接线:`LocalFileSystem`、`Config`、每目标变更锁 |
80
+ | [`src/fsio.ts`](src/fsio.ts) | 不依赖 Cordis 的原始 I/O:探测、读取、原子写入、字面量编辑、行尾处理 |
81
+ | [`src/win32.ts`](src/win32.ts) | 原子替换的 Windows 专属 DACL 保留 |
82
+
83
+ ### 写入路径
84
+
85
+ 每次写入先探测目标、执行可选防护(`createIfAbsent` 或 `replaceIfVersion`)、在两侧都足够小时捕获有界的 `before` diff 基础、把新内容暂存到目标旁、fsync,然后原子发布。带防护的创建使用绝不替换并发创建者的硬链接发布,并以 `FS_NOT_OBSERVED` 拒绝它。
86
+
87
+ ### 编辑路径
88
+
89
+ 每次编辑先探测、在字面量匹配前校验版本防护(陈旧编辑因此报告 `FS_STALE_VERSION`,绝不会给出误导性的无匹配)、读取文件、以 LF 规范化执行字面量替换、恢复文件主要的行尾风格,然后重新发布——全部在每目标锁内完成。
90
+
91
+ ### 归属与不变式
92
+
93
+ 原始 I/O 不依赖 Cordis,在 `src/fsio.ts` 中独立单元测试;`src/index.ts` 保持为轻量接线。`config.cwd` 只是解析默认值——约束是 `fs-sandbox` 或 `tools/execute` 权限插件的工作。取消是尽力而为的 `AbortSignal`,在每次异步探测前后检查。
94
+
95
+ </details>
96
+
97
+ -----
98
+
99
+ <a id="further-exploration"></a>
100
+ ## 进一步探索
101
+
102
+ 当包级约定不够用时阅读以下页面。它们从约定逐步进入相邻的后端、工具与策略。
103
+
104
+ - [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。
105
+ - [dsh-fs](../fs/README.zh.md)——本后端实现的 `ctx.fs` 约定。
106
+ - [fs-sandbox](../fs-sandbox/README.zh.md)——扩展本后端的沙箱强制后端。
107
+ - [tool-fs](../tool-fs/README.zh.md)——消费 `ctx.fs` 的面向模型工具。
108
+ - [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。
109
+ - [Windows DACL 保留笔记](../../../.agents/notes/implemented/bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md)——原子替换为何复制目标的访问策略。
110
+
111
+ -----
112
+
113
+ <a id="model-experience"></a>
28
114
  ## 模型体验
29
115
 
30
- 通过 [`dsh-tool-fs`](../tool-fs/README.zh.md) 间接产生影响;该消费方把本提供方带行窗口的 UTF-8 内容、变更确认和提供方消息原文渲染为有保留上限的结果,而版本、原子写入机制和目录元数据仍属内部细节。
116
+ 通过 `dsh-tool-fs` 间接产生影响;该消费方把本提供方带行窗口的 UTF-8 内容、变更确认与提供方消息原文渲染为有保留上限的结果,而版本、原子写入机制与目录元数据仍属内部细节。
31
117
 
32
118
  #### KV Cache 影响
33
119
 
@@ -35,11 +121,26 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() })
35
121
 
36
122
  ## 已知限制与延期工作
37
123
 
38
- - **`config.cwd` 不是沙箱**:它是解析默认值,而非约束;绝对路径和 `..` 可以逃逸。请使用更严格的 `ctx.fs` 后端或 `tools/execute` waterfall(瀑布式事件)上的权限插件实施约束(见[能力 seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md#consequences))。
39
- - **版本 token 依赖文件系统元数据**:它们组合设备、inode、大小、纳秒级 mtime 和纳秒级 ctime;如果存储层在重写时无法更新其中任何一项事实,仍可能绕过陈旧防护。
124
+ <a id="known-limitations-and-deferred-work"></a>
125
+
126
+
127
+ 这些限制说明本地后端何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用文件系统对比或任务积压。
128
+
129
+ - **`config.cwd` 不是沙箱**:它是解析默认值,而非约束;绝对路径和 `..` 可以逃逸。请使用更严格的 `ctx.fs` 后端或 `tools/execute` waterfall(瀑布式事件)上的权限插件实施约束(见[能力 seam 笔记](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md))。
130
+ - **版本 token 依赖文件系统元数据**:它们组合设备、inode、大小、纳秒级 mtime 与纳秒级 ctime;如果存储层在重写时无法更新其中任何一项事实,仍可能绕过陈旧防护。
40
131
  - **`editText` 会把整个文件及编辑后的副本保存在内存中**:只有读取路径支持流式处理。
41
- - **低于上限的覆写仍会缓冲上下文基础**:`writeText` 除调用方持有的替换内容外,最多还会保留略低于 `config.diffBasisMaxBytes` 的旧文本;该上限不限制返回的 `after` 值,也不限制展示层的整文件回退。
132
+ - **低于上限的覆写仍会缓冲上下文基础**:`writeText` 除调用方持有的替换内容外,最多还会保留略低于 `config.diffBasisMaxBytes` 的旧文本;该上限不限制返回的 `after` 值,也不限制整文件展示回退。
42
133
  - **二进制检测不对称**:读取只对前 8192 字节执行 NUL 采样,编辑则扫描整个 buffer,因此 NUL 出现在后部的文件可以读取,但编辑会被拒绝。
43
134
  - **每目标变更锁仅限进程内**:即使跨进程,带防护的创建仍采用原子且不替换的发布方式;但只有当可选版本防护观察到元数据变化时,系统才能发现其他进程中的替换写入方,且绝不会将其串行化。
44
- - **带防护的创建要求支持硬链接**:拒绝硬链接发布的文件系统或挂载点无法支持 `createIfAbsent`;提供方会使目标保持缺失状态并报告 `FS_IO_ERROR`。
135
+ - **带防护的创建要求支持硬链接**:拒绝硬链接发布的文件系统或挂载点无法支持 `createIfAbsent`;后端会使目标保持缺失状态并报告 `FS_IO_ERROR`。
45
136
  - **提交后清理采用尽力而为语义**:如果移除仅所有者可访问的暂存目录失败,成功发布仍视为成功,并留下私有残留供运维人员后续清理。
137
+
138
+ <a id="dev-note"></a>
139
+ ### 开发备注
140
+
141
+ <details>
142
+ <summary>维护者的工作上下文——点击展开</summary>
143
+
144
+ 无。
145
+
146
+ </details>
package/lib/index.js CHANGED
@@ -713,6 +713,9 @@ var LocalFileSystem = class extends FileSystem {
713
713
  processPath(target) {
714
714
  return String(target.targetKey);
715
715
  }
716
+ processPathFromHostPath(hostPath) {
717
+ return isAbsolute(hostPath) ? resolve(hostPath) : void 0;
718
+ }
716
719
  fileUrl(target) {
717
720
  return pathToFileURL(this.processPath(target)).href;
718
721
  }
@@ -43,6 +43,7 @@ export declare class LocalFileSystem extends FileSystem {
43
43
  signal?: AbortSignal;
44
44
  }): Promise<FsTarget>;
45
45
  processPath(target: FsTarget): string;
46
+ processPathFromHostPath(hostPath: string): string | undefined;
46
47
  fileUrl(target: FsTarget): string;
47
48
  contains(parent: FsTarget, child: FsTarget): boolean;
48
49
  stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-fs-local",
3
3
  "description": "Local-filesystem implementation of the DeepSeek Harness filesystem seam (ctx.fs)",
4
- "version": "0.1.1-rc.1",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,18 +32,18 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/dsh-fs": "^0.1.1-rc.1",
36
- "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1"
35
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
36
+ "@deepseek-ai/cordis": "^4.0.2",
37
+ "@deepseek-ai/dsh-fs": "^0.1.2-alpha.2"
38
38
  },
39
39
  "dependencies": {
40
40
  "koffi": "^3.1.0",
41
- "@deepseek-ai/schemastery": "^3.18.1"
41
+ "@deepseek-ai/schemastery": "^3.18.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@deepseek-ai/dsh-fs": "^0.1.1-rc.1",
45
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
46
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.1",
47
- "@deepseek-ai/cordis": "^4.0.1"
44
+ "@deepseek-ai/dsh-fs": "^0.1.2-alpha.2",
45
+ "@deepseek-ai/cordis": "^4.0.2",
46
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
47
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2"
48
48
  }
49
49
  }