@deepseek-ai/dsh-fs-local 0.1.5-rc.2 → 0.1.6-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: 427cf02631f9d56246851f7fc67c92dd662c2a62
6
- README.zh.md: 7009d9dd853b5443780978036792f10ebc9706b5
5
+ README.md: e781abd2032868a3768a41eb01ce5aac52f8c835
6
+ README.zh.md: bf21f92a48c7c9d804908ee8bc25b2be8daa8967
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations or `fs-e2b` for files in a remote execution world.
12
+ Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -29,11 +29,11 @@ Mount this backend when a composition needs `ctx.fs` backed by the real host fil
29
29
 
30
30
  ### When to choose it
31
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.
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. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it.
33
33
 
34
34
  ### Minimal configuration
35
35
 
36
- Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it.
36
+ Load the backend with a base directory; relative paths resolve against it, and absolute paths ignore it. A relative base is anchored to the provider process working directory, and display paths remain absolute. On POSIX, resolution follows filesystem semantics before lexical normalization: `symlink/..` reaches the parent of the link target, including when the final file does not exist yet. Directory listings preserve the same physical traversal in displayed child paths. Windows retains native drive-relative normalization.
37
37
 
38
38
  ```yaml
39
39
  - name: '@deepseek-ai/dsh-fs-local'
@@ -50,9 +50,9 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
50
50
 
51
51
  ### What you can do
52
52
 
53
- Read any regular UTF-8 text file whole or as a stream, read raw bytes up to a cap you choose or as one byte window, 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.
53
+ Read any regular UTF-8 text file whole or as a stream, read raw bytes up to a cap you choose or in a byte window, 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
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.
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 an edit reports `FS_STALE_VERSION` whether or not the version guard is supplied.
56
56
 
57
57
  -----
58
58
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox`,文件位于远程执行世界时使用 `fs-e2b`。
12
+ 使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox`。
13
13
 
14
14
  ## 目录
15
15
 
@@ -29,11 +29,11 @@ kind: "package-reference"
29
29
 
30
30
  ### 何时选择
31
31
 
32
- 普通宿主文件访问请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。文件必须位于与子进程共享的远程执行世界时,选择 [`fs-e2b`](../../e2b/fs-e2b/README.zh.md)。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
32
+ 在单个进程中进行普通宿主文件访问时,请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
33
33
 
34
34
  ### 最小配置
35
35
 
36
- 加载后端并给出基准目录;相对路径以它为基准解析,绝对路径忽略它。
36
+ 用一个基础目录加载后端;相对路径基于它解析,绝对路径则忽略它。相对基础目录以提供方进程工作目录为起点,展示路径始终保持绝对路径。在 POSIX 上,解析先遵循文件系统语义,再进行词法规范化:`symlink/..` 到达链接目标的父目录,即使最终文件尚不存在也如此。目录列表中的子项展示路径保留同样的物理遍历语义。Windows 保留原生的驱动器相对路径规范化行为。
37
37
 
38
38
  ```yaml
39
39
  - name: '@deepseek-ai/dsh-fs-local'
@@ -46,13 +46,13 @@ kind: "package-reference"
46
46
  | `cwd` | `process.cwd()` | 相对路径的基准目录 |
47
47
  | `diffBasisMaxBytes` | `10 MiB` | 每次覆写 diff 一侧的 UTF-8 字节上限;更大的覆写返回 `before: null` |
48
48
 
49
- 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-fs-local)是每个受支持字段及其 JSDoc 的穷尽式真源。
49
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-fs-local)完整列出了所有受支持字段及其 JSDoc。
50
50
 
51
51
  ### 你能做什么
52
52
 
53
53
  完整或流式读取任意普通 UTF-8 文本文件,按你选择的上限或按字节窗口读取原始字节,并按稳定名称顺序列出一层目录。原子地创建或替换文件,并原子地应用字面量文本编辑;两个变更操作都按文件串行化,并发写入方绝不会交错。版本防护是可选的:省略它即无条件创建或覆盖,提供它则在文件自上次观察以来发生变化时失败。
54
54
 
55
- 失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。带防护的编辑遇到缺失目标时,无论哪种情况都报告 `FS_STALE_VERSION`。
55
+ 失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。编辑遇到缺失目标时,无论是否提供版本防护,都报告 `FS_STALE_VERSION`。
56
56
 
57
57
  -----
58
58
 
@@ -69,8 +69,8 @@ kind: "package-reference"
69
69
  后端建立在三个想法之上:
70
70
 
71
71
  - **Realpath 身份。** `targetKey` 是文件的 `realpath`,因此经符号链接到达同一文件的两个输入路径共享一个身份,写入落在链接目标上,同时保留链接。
72
- - **原子发布。** 写入先写入目标旁私有暂存目录内的独占临时文件,执行 fsync 后发布;现有文件的 mode 会保留,Windows 上的 DACL 也会在替换后存活。
73
- - **单一变更临界区。** 每目标 FIFO 锁串行化读取→防护→写入窗口,并发写入与编辑因此被确定性排序——一方胜出,其余看到新版本并以陈旧拒绝。
72
+ - **原子发布。** 写入先写入目标旁私有暂存目录内的独占临时文件,执行 fsync 后发布;现有文件的 mode 会保留,Windows 上的 DACL 在替换后也会保留。
73
+ - **单一变更临界区。** 每目标 FIFO 锁串行化读取→防护→写入窗口,并发写入与编辑因此被确定性排序——一方胜出,其余操作看到新版本后因版本陈旧而被拒绝。
74
74
 
75
75
  ### 源码地图
76
76
 
package/lib/index.js CHANGED
@@ -4,9 +4,9 @@ import { pathToFileURL } from "node:url";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { FileSystem, FsError, FsTargetKey, FsVersion } from "@deepseek-ai/dsh-fs";
6
6
  import { randomUUID } from "node:crypto";
7
- import { createReadStream } from "node:fs";
8
- import { chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
9
- import { TextDecoder } from "node:util";
7
+ import { createReadStream, realpath } from "node:fs";
8
+ import { chmod, link, lstat, mkdir, open, readFile, readdir, rename, rm, stat } from "node:fs/promises";
9
+ import { TextDecoder, promisify } from "node:util";
10
10
  //#region lib/types/win32.js
11
11
  /**
12
12
  * Windows security-descriptor helpers for atomic local-file replacement. Koffi loads lazily so
@@ -93,6 +93,7 @@ async function replaceFileWin32(replaced, replacement) {
93
93
  * @module @deepseek-ai/dsh-fs-local/fsio
94
94
  */
95
95
  const BINARY_SAMPLE_BYTES = 8192;
96
+ const realpath$1 = promisify(realpath.native);
96
97
  const DIFF_BASIS_READ_CHUNK_BYTES = 64 * 1024;
97
98
  function isENOENT(error) {
98
99
  return error instanceof Error && "code" in error && error.code === "ENOENT";
@@ -143,6 +144,19 @@ function versionOf(info) {
143
144
  return FsVersion(`${info.dev}:${info.ino}:${info.size}:${info.mtimeNs}:${info.ctimeNs}`);
144
145
  }
145
146
  /**
147
+ * Anchor a path using native drive semantics and POSIX physical parent traversal.
148
+ * @param cwd - provider base directory for relative paths.
149
+ * @param path - non-empty requested path.
150
+ * @returns absolute display spelling shared by target resolution and no-follow metadata.
151
+ */
152
+ function localDisplayPath(cwd, path) {
153
+ const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}`;
154
+ const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}`;
155
+ const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path);
156
+ /* v8 ignore next -- Native Windows tests cover DOS drive-relative resolution; POSIX preserves physical traversal. */
157
+ return process.platform === "win32" ? resolve(cwd, path) : physicalSpelling;
158
+ }
159
+ /**
146
160
  * Resolve a path to its absolute display path and realpath identity. For a missing target,
147
161
  * realpath the nearest existing ancestor and append the missing suffix, preserving identity
148
162
  * across symlinked ancestors before and after creation.
@@ -152,11 +166,11 @@ function versionOf(info) {
152
166
  */
153
167
  async function resolveLocalTarget(cwd, path) {
154
168
  if (path.trim().length === 0) throw new FsError("file_path must be a non-empty string", "FS_NOT_FOUND");
155
- const displayPath = resolve(cwd, path);
169
+ const displayPath = localDisplayPath(cwd, path);
156
170
  try {
157
171
  return {
158
172
  displayPath,
159
- targetKey: FsTargetKey(await realpath(displayPath))
173
+ targetKey: FsTargetKey(await realpath$1(displayPath))
160
174
  };
161
175
  } catch (error) {
162
176
  /* v8 ignore next -- Windows reports this case as ENOENT and repairs it in the ancestor walk below. */
@@ -167,7 +181,9 @@ async function resolveLocalTarget(cwd, path) {
167
181
  const missing = [basename(displayPath)];
168
182
  let ancestor = dirname(displayPath);
169
183
  while (true) try {
170
- const realAncestor = await realpath(ancestor);
184
+ const realAncestor = await realpath$1(ancestor);
185
+ /* v8 ignore next -- POSIX rejects this traversal; Windows normalizes parent segments before filesystem lookup. */
186
+ if (missing.includes("..")) throw new FsError(`cannot resolve "${displayPath}": parent traversal crosses a missing directory`, "FS_NOT_FOUND");
171
187
  /* v8 ignore start -- native Windows coverage exercises this repair; POSIX reports ENOTDIR before this point. */
172
188
  if (process.platform === "win32") {
173
189
  if (!(await stat(realAncestor)).isDirectory()) throw new FsError(`cannot resolve "${displayPath}": a parent path segment is not a directory`, "FS_NOT_FOUND");
@@ -254,7 +270,7 @@ function listingIoError(displayPath, error) {
254
270
  async function resolveListedChildTarget(parent, name) {
255
271
  const identity = await resolveLocalTarget(parent.targetKey, name);
256
272
  return {
257
- displayPath: join(parent.displayPath, name),
273
+ displayPath: localDisplayPath(parent.displayPath, name),
258
274
  targetKey: identity.targetKey
259
275
  };
260
276
  }
@@ -301,7 +317,7 @@ async function listDirectory(target, signal) {
301
317
  ...childInfo?.type === "file" ? { size: childInfo.size } : {}
302
318
  });
303
319
  } catch (error) {
304
- throw listingIoError(join(target.displayPath, entry.name), error);
320
+ throw listingIoError(localDisplayPath(target.displayPath, entry.name), error);
305
321
  }
306
322
  throwIfAborted(signal, "list");
307
323
  }
@@ -770,7 +786,7 @@ var LocalFileSystem = class extends FileSystem {
770
786
  async lstat(path, opts, signal) {
771
787
  if (signal?.aborted) throw new FsError("lstat aborted", "FS_ABORTED");
772
788
  if (path.trim().length === 0) throw new FsError("file_path must be a non-empty string", "FS_NOT_FOUND");
773
- const info = await probeNoFollow(resolve(opts?.cwd ?? this.config.cwd, path));
789
+ const info = await probeNoFollow(localDisplayPath(opts?.cwd ?? this.config.cwd, path));
774
790
  if (signal?.aborted) throw new FsError("lstat aborted", "FS_ABORTED");
775
791
  if (!info) return void 0;
776
792
  return {
@@ -64,6 +64,13 @@ export interface LocalDirEntry {
64
64
  version?: FsVersion;
65
65
  size?: number;
66
66
  }
67
+ /**
68
+ * Anchor a path using native drive semantics and POSIX physical parent traversal.
69
+ * @param cwd - provider base directory for relative paths.
70
+ * @param path - non-empty requested path.
71
+ * @returns absolute display spelling shared by target resolution and no-follow metadata.
72
+ */
73
+ export declare function localDisplayPath(cwd: string, path: string): string;
67
74
  /**
68
75
  * Resolve a path to its absolute display path and realpath identity. For a missing target,
69
76
  * realpath the nearest existing ancestor and append the missing suffix, preserving identity
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.5-rc.2",
4
+ "version": "0.1.6-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "peerDependencies": {
30
- "@deepseek-ai/dsh-fs": "^0.1.5-rc.2",
30
+ "@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
31
31
  "@deepseek-ai/cordis": "^4.0.2"
32
32
  },
33
33
  "dependencies": {
@@ -35,8 +35,8 @@
35
35
  "@deepseek-ai/schemastery": "^3.18.2"
36
36
  },
37
37
  "devDependencies": {
38
- "@deepseek-ai/dsh-fs": "^0.1.5-rc.2",
39
- "@deepseek-ai/cordis": "^4.0.2",
40
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2"
38
+ "@deepseek-ai/dsh-fs": "^0.1.6-alpha.2",
39
+ "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
40
+ "@deepseek-ai/cordis": "^4.0.2"
41
41
  }
42
42
  }