@qilin-agent/fs 0.0.0-stage → 3.1.4

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 kkutysllb
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,58 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/fs/fs/README.md
5
+ /:
6
+ en: a94eb1470c498a2c
7
+ zh: cdd5faa0a8fcb562
8
+ /qilin-agent-fs:
9
+ en: 36ec08f75c970d6b
10
+ zh: 1cad13ef88ce2063
11
+ /qilin-agent-fs/summary:
12
+ en: d86cd3418ce07313
13
+ zh: e8a6d9e3e530e5ad
14
+ /qilin-agent-fs/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /qilin-agent-fs/use-this-package:
18
+ en: c935aeef50118bbc
19
+ zh: 218ca6a49545668f
20
+ /qilin-agent-fs/use-this-package/choosing-and-mounting-a-backend:
21
+ en: 6644d749bb66bd8d
22
+ zh: 929dcc5cd4ee70ea
23
+ /qilin-agent-fs/use-this-package/what-the-service-lets-you-do:
24
+ en: 7362a6df4c2f9b60
25
+ zh: f1120bcaada1037d
26
+ /qilin-agent-fs/understand-the-implementation:
27
+ en: 7e13d669ecc96d59
28
+ zh: b5fe7ae65812d926
29
+ /qilin-agent-fs/understand-the-implementation/design-philosophy:
30
+ en: c4dcad50dc90db2c
31
+ zh: 54775c7bcdc24d86
32
+ /qilin-agent-fs/understand-the-implementation/source-map:
33
+ en: 038de0a4127c1ff5
34
+ zh: 192947b527c3f258
35
+ /qilin-agent-fs/understand-the-implementation/how-a-call-flows:
36
+ en: 9272eec1c2b30667
37
+ zh: 1e5c82060097fd1e
38
+ /qilin-agent-fs/understand-the-implementation/the-fs-policy-events:
39
+ en: 64bb44beb5e68508
40
+ zh: 306dafa5e3fe39af
41
+ /qilin-agent-fs/understand-the-implementation/invariants:
42
+ en: 15b98bc9800b6eae
43
+ zh: 434d9dd05fa1a332
44
+ /qilin-agent-fs/further-exploration:
45
+ en: bc28a173da8fd4db
46
+ zh: 9e981417d86b72a5
47
+ /qilin-agent-fs/model-experience:
48
+ en: cd1c5414ab00174f
49
+ zh: 6c7ce64b9e618f93
50
+ /qilin-agent-fs/model-experience/kv-cache-effect:
51
+ en: 40294c1c31b74824
52
+ zh: 0857e9b4dd363e0a
53
+ /qilin-agent-fs/known-limitations-and-deferred-work:
54
+ en: be13bf5e7c376d92
55
+ zh: ff95ba8f51eb4dcc
56
+ /qilin-agent-fs/known-limitations-and-deferred-work/dev-note:
57
+ en: e67f633dc6fe7773
58
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,129 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "The ctx.fs filesystem service contract for deployments choosing or mounting a filesystem backend and developers implementing one."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @qilin-agent/fs
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Use `qilin-fs` when an application needs consistent filesystem operations across host, confined, or remote execution environments. It lets consumers resolve stable file identities, map shared host files where supported, perform bounded text and byte reads, list directories, watch targets, apply atomic text writes and literal edits, and remove, move, or create directories. Version guards are optional, so a backend works without policy enforcement; callers can supply a guard to reject a mutation after the file changes. Choose `fs-local` or `fs-sandbox` for host execution. Model-facing filesystem tools are provided separately by `qilin-tool-fs`.
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
+ You rarely load `qilin-fs` directly: you mount a backend that registers as `ctx.fs`, then either call the service from your own plugin or let the `qilin-tool-fs` tools call it for you. This page serves the two audiences that do touch it — deployments choosing a backend, and developers implementing or consuming the contract.
29
+
30
+ ### Choosing and mounting a backend
31
+
32
+ Pick [`fs-local`](../fs-local/README.md) for ordinary host files or [`fs-sandbox`](../fs-sandbox/README.md) when a session's mutations must be confined to its workspace and temp roots. Mounting any backend populates `ctx.fs`; swapping backends changes nothing for the policy plugin, the tools, or the tool schemas. A composition that mounts no backend has no `ctx.fs` at all, and the tools fail at registration.
33
+
34
+ ### What the service lets you do
35
+
36
+ Through `ctx.fs` you can resolve any path to a stable target identity, read a whole text file or stream it in chunks, read raw bytes up to an explicit cap, list one directory level, atomically create or replace a file, apply a literal text edit atomically, remove a file or a directory, move or rename an entry inside one backend, and create a directory (with its missing parents when asked). The version guard on file writes and edits is optional: omit it for unconditional create-or-overwrite, or supply it to fail when the file changed since you last observed it. Read, listing, and mutation failures are typed `FsError`s with stable codes such as `FS_NOT_FOUND`, `FS_STALE_VERSION`, or `FS_AMBIGUOUS_EDIT`, so callers branch on the code, never on message text.
37
+
38
+ `remove`, `move`, and `createDirectory` keep that vocabulary: an absent target is `FS_NOT_FOUND`, a directory removed without `recursive` while it still holds entries is `FS_NOT_EMPTY`, and a create or a move onto an entry that already exists is `FS_EXISTS`. A backend that cannot perform the move at all — a rename across filesystems, for instance — reports `FS_IO_ERROR` instead of copying the content.
39
+
40
+ `watch(target, changed, signal)` reports invalidations for one file or a directory's direct entries. It resolves once observation is ready with an asynchronous close function that the caller must await. The signal cancels initialization; unsupported providers reject without polling.
41
+
42
+ -----
43
+
44
+ <a id="understand-the-implementation"></a>
45
+ ## Understand the implementation
46
+
47
+ <details>
48
+ <summary>Implementation internals — click to expand</summary>
49
+
50
+ This section explains the design decisions behind the contract and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
51
+
52
+ ### Design philosophy
53
+
54
+ The contract is built on one separation and three commitments:
55
+
56
+ - **Contract over mechanism.** The service names what a storage layer can do — resolve, stat, read, list, watch, write, edit, remove, move, createDirectory — and never how it stores bytes. Backends own target identity, execution-world coordinates, decoding, binary rejection, and atomicity.
57
+ - **Policy stays off the base class.** Observed-state, read-before-edit, and version-guarded mutations are a plugin's job (`qilin-fs-observation-policy`), added by supplying the optional guard — so a sandboxed or remote backend inherits no model-facing observation policy.
58
+ - **`editText` stays on the seam.** Version check, literal match, and atomic rewrite share one critical section, so error attribution and one-wins/one-stale concurrency stay correct; a remote backend may implement it as a native compare-and-edit.
59
+ - **Bounds live at this seam.** `readBytes` requires `maxBytes` and fails with `FS_TOO_LARGE` rather than truncating, so no backend ever buffers an unbounded file. `readByteRange` is bounded by its window instead: a backend transfers at most the requested `length` beyond the prefix it skips, so the caller's cap on `length` is the guard.
60
+
61
+ ### Source map
62
+
63
+ | File | Role |
64
+ |---|---|
65
+ | [`src/index.ts`](src/index.ts) | Service definition: the abstract `FileSystem` class, the `ctx.fs` declaration, and the `fs/*` event vocabulary |
66
+ | [`src/types.ts`](src/types.ts) | Vocabulary: `FsTarget`/`FsTargetKey`, `FsVersion`, `FsObservation`, `FsWriteIntent`, `FsRemoveOutcome`, `FsMoveOutcome`, `FsError` and its codes |
67
+
68
+ ### How a call flows
69
+
70
+ Every ordinary operation starts with `resolve(path, { cwd })`, which produces a stable `FsTarget` (an opaque `targetKey` plus a `displayPath` for model/UI output); the same file reached through different paths yields the same key. `processPathFromHostPath(hostPath)` separately maps an absolute host file into this execution world when the backend shares or explicitly maps it, and otherwise returns `undefined`. Reads then go `stat` → `readText`/`streamText`/`readBytes`/`readByteRange`, listings go `listDir` (whose entries report the resolved type plus a `symlink` flag when the listed name itself is a link), and mutations go through one per-target critical section: the optional guard is checked, the new content is applied, and the result is published atomically. A move holds the critical section of both ends, taken in a fixed key order, so two moves touching the same pair of targets serialize instead of deadlocking; the other two take the single lock of the target they touch.
71
+
72
+ ### The `fs/*` policy events
73
+
74
+ The package declares three events so the emitter (`qilin-tool-fs`) and the policy listener (`qilin-fs-observation-policy`) share a vocabulary without the emitter depending on the policy plugin. `fs/write-intent` and `fs/edit-intent` are single-slot decision waterfalls: the first listener decides outright and never calls `next()`. `fs/observed` is a fire-and-forget recording event carrying an `FsObservation` — present with a version, or confirmed absent. The events carry only `qilin-fs` vocabulary plus an opaque `object` actor.
75
+
76
+ ### Invariants
77
+
78
+ - `targetKey` and `version` are branded opaque ids: consumers must not parse or interpret them; only `displayPath` is for model/UI output.
79
+ - Read, listing, and mutation failures are typed `FsError`s with stable codes, never ad-hoc message strings; watch initialization may reject with an ordinary exception.
80
+ - The seam arms no I/O deadline; cancellation is an optional per-primitive `AbortSignal`.
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## Further Exploration
88
+
89
+ Read these pages when the package-level contract is not enough. They move from the exhaustive contract to the backends and consumers built on it.
90
+
91
+ - [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy.
92
+ - [fs-local](../fs-local/README.md) — the host-filesystem backend implementing this contract.
93
+ - [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend implementing this contract.
94
+ - [tool-fs](../tool-fs/README.md) — the model-facing tools that consume `ctx.fs`.
95
+ - [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events.
96
+ - [Capability seams note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) — why the filesystem stack splits into contract, provider, policy, and tools.
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
101
+ ## Model Experience
102
+
103
+ Indirectly, through `qilin-tool-fs`, which renders provider text and errors as bounded, retained filesystem tool results.
104
+
105
+ #### KV Cache effect
106
+
107
+ No direct invalidation; the named consumer owns any request-prefix changes.
108
+
109
+ ## Known Limitations and Deferred Work
110
+
111
+ <a id="known-limitations-and-deferred-work"></a>
112
+
113
+
114
+ These limits define when the contract is a poor fit or needs special operational care. They are current package constraints, not a general filesystem comparison or a task backlog.
115
+
116
+ - **Text-only mutations by contract** — text reads and both mutations reject binary or non-UTF-8 content with `FS_NOT_TEXT`; `readBytes` and `readByteRange` are the raw-byte primitives, and binary-safe mutations remain deferred.
117
+ - **No copy** — a move stays inside one backend and never copies across filesystems, and `listDir` still lists a single level, with globbing, pagination, and search out of scope ([directory-listing note](../../../.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.md)).
118
+ - **No I/O deadline** — the seam arms no timeout; cancellation is a best-effort optional `AbortSignal` per primitive ([fs family stance](../README.md)).
119
+ - **Resolve-then-operate costs a remote backend two round-trips per tool call** — folding or caching resolution is left to such a backend.
120
+
121
+ <a id="dev-note"></a>
122
+ ### Dev Note
123
+
124
+ <details>
125
+ <summary>Working context for maintainers — click to expand</summary>
126
+
127
+ None.
128
+
129
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,129 @@
1
+ ---
2
+ description: "`ctx.fs` 文件系统服务约定:面向选择或挂载文件系统后端的部署方,以及实现后端的开发者。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @qilin-agent/fs
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 应用需要在宿主、受限或远程执行环境中使用一致的文件系统操作时,选择 `qilin-fs`。消费方可以解析稳定的文件身份、在受支持时映射共享宿主文件、执行有界的文本与字节读取、列出目录、监听目标、原子地写入文本及执行字面量编辑,以及删除、移动或新建目录。版本防护是可选的,因此后端无需策略强制也能工作;调用方可以提供防护,在文件变化后拒绝变更。在宿主执行时选择 `fs-local` 或 `fs-sandbox`。面向模型的文件系统工具由 `qilin-tool-fs` 单独提供。
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
+ 你很少直接加载 `qilin-fs`:你挂载一个注册为 `ctx.fs` 的后端,然后从自己的插件调用该服务,或让 `qilin-tool-fs` 工具替你调用。本页服务于确实接触它的两类读者——选择后端的部署方,以及实现或消费该约定的开发者。
29
+
30
+ ### 选择并挂载后端
31
+
32
+ 普通宿主文件选择 [`fs-local`](../fs-local/README.zh.md),会话变更必须限制在工作区与临时根目录内时选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)。挂载任一后端都会填充 `ctx.fs`;更换后端不会改变策略插件、工具或工具 schema。未挂载任何后端的组合就没有 `ctx.fs`,工具会在注册时失败。
33
+
34
+ ### 服务能做什么
35
+
36
+ 通过 `ctx.fs`,你可以把任意路径解析为稳定的目标身份、完整读取或分片流式读取文本文件、按显式上限读取原始字节、列出一层目录、原子地创建或替换文件、原子地应用字面量文本编辑、删除文件或目录、在同一后端内移动或重命名一个条目,以及新建目录(按需连同缺失的父目录)。文件写入与编辑上的版本防护是可选的:省略它即无条件创建或覆盖,提供它则在文件自上次观察以来发生变化时失败。读取、列出与变更操作的失败使用携带稳定错误码(如 `FS_NOT_FOUND`、`FS_STALE_VERSION`、`FS_AMBIGUOUS_EDIT`)的类型化 `FsError`,调用方依据错误码分支,绝不解析消息文本。
37
+
38
+ `remove`、`move` 与 `createDirectory` 沿用同一套词汇:目标不存在是 `FS_NOT_FOUND`;未带 `recursive` 而目录仍有条目时删除是 `FS_NOT_EMPTY`;新建或移动落在一个已存在的条目上是 `FS_EXISTS`。后端根本无法执行该移动时——例如跨文件系统重命名——报告 `FS_IO_ERROR`,而不是改为复制内容。
39
+
40
+ `watch(target, changed, signal)` 报告单个文件或目录直接子项的失效通知。观察就绪后,它返回调用方必须等待完成的异步关闭函数。signal 取消初始化;不支持监听的提供方直接拒绝,不使用轮询。
41
+
42
+ -----
43
+
44
+ <a id="understand-the-implementation"></a>
45
+ ## 理解实现
46
+
47
+ <details>
48
+ <summary>实现细节——点击展开</summary>
49
+
50
+ 本节解释约定背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
51
+
52
+ ### 设计理念
53
+
54
+ 该约定建立在一个分离与三项承诺之上:
55
+
56
+ - **约定高于机制。** 服务只命名存储层能做什么——解析、stat、读取、列出、监听、写入、编辑、删除、移动、新建目录——绝不规定如何存储字节。后端拥有目标身份、执行世界坐标、解码、二进制拒绝与原子性。
57
+ - **策略不放在基类上。** 已观察状态、编辑前读取与版本防护的变更是插件(`qilin-fs-observation-policy`)的职责,通过提供可选防护来添加——因此沙箱化或远程后端不会继承任何面向模型的观察策略。
58
+ - **`editText` 留在 seam 上。** 版本校验、字面量匹配与原子重写共享同一个临界区,错误归因与一方胜出/一方陈旧的并发语义因此保持正确;远程后端也可以将其实现为原生比较并编辑操作。
59
+ - **界限制在此 seam 上。** `readBytes` 要求 `maxBytes`,并以 `FS_TOO_LARGE` 失败而不是截断,因此任何后端都不会无界缓冲文件。`readByteRange` 则以窗口为界:后端最多传输所请求的 `length` 字节(外加为到达 `offset` 而跳过的前缀),因此由调用方对 `length` 的上限承担防护。
60
+
61
+ ### 源码地图
62
+
63
+ | 文件 | 职责 |
64
+ |---|---|
65
+ | [`src/index.ts`](src/index.ts) | 服务定义:抽象 `FileSystem` 类、`ctx.fs` 声明与 `fs/*` 事件词汇 |
66
+ | [`src/types.ts`](src/types.ts) | 词汇:`FsTarget`/`FsTargetKey`、`FsVersion`、`FsObservation`、`FsWriteIntent`、`FsRemoveOutcome`、`FsMoveOutcome`、`FsError` 及其错误码 |
67
+
68
+ ### 调用流程
69
+
70
+ 每个普通操作都以 `resolve(path, { cwd })` 开始,它产生稳定的 `FsTarget`(不透明 `targetKey` 加用于模型/UI 输出的 `displayPath`);经不同路径到达同一文件会产生相同 key。`processPathFromHostPath(hostPath)` 在后端共享或显式映射宿主文件时,单独把绝对宿主文件映射进此执行世界,否则返回 `undefined`。读取随后执行 `stat` → `readText`/`streamText`/`readBytes`/`readByteRange`,列出执行 `listDir`(条目报告解析后的类型,名字本身是链接时另带 `symlink` 标志),变更则经过每个目标一个临界区:先检查可选防护,应用新内容,再原子发布结果。移动会同时持有两端的临界区,并按固定的 key 顺序取锁,因此触碰同一对目标的两个移动会串行而不是死锁;另外两个只取自己所触目标的单把锁。
71
+
72
+ ### `fs/*` 策略事件
73
+
74
+ 本包声明三个事件,使发出方(`qilin-tool-fs`)与策略监听器(`qilin-fs-observation-policy`)共享词汇,而无需让发出方依赖策略插件。`fs/write-intent` 与 `fs/edit-intent` 是单槽决策 waterfall(瀑布式事件):第一个监听器直接决策,绝不调用 `next()`。`fs/observed` 是发后即忘的记录事件,携带 `FsObservation`——存在并带版本,或确认缺失。事件只携带 `qilin-fs` 词汇和一个不透明 `object` 参与者。
75
+
76
+ ### 不变式
77
+
78
+ - `targetKey` 与 `version` 是带品牌的不透明 id:消费方不得解析或解释它们;只有 `displayPath` 用于模型/UI 输出。
79
+ - 读取、列出与变更操作的失败是携带稳定错误码的类型化 `FsError`,绝不是临时拼写的消息字符串;监听初始化可以用普通异常拒绝。
80
+ - 该 seam 不设 I/O deadline;取消是每个原语上可选的 `AbortSignal`。
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## 进一步探索
88
+
89
+ 当包级约定不够用时阅读以下页面。它们从穷尽式约定逐步进入构建于其上的后端与消费方。
90
+
91
+ - [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。
92
+ - [fs-local](../fs-local/README.zh.md)——实现该约定的宿主文件系统后端。
93
+ - [fs-sandbox](../fs-sandbox/README.zh.md)——实现该约定的沙箱强制后端。
94
+ - [tool-fs](../tool-fs/README.zh.md)——消费 `ctx.fs` 的面向模型工具。
95
+ - [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。
96
+ - [能力 seam 笔记](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md)——文件系统栈为何拆分为约定、提供方、策略与工具。
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
101
+ ## 模型体验
102
+
103
+ 通过 `qilin-tool-fs` 间接产生影响;该消费方把提供方文本和错误渲染为有界且保留的文件系统工具结果。
104
+
105
+ #### KV Cache 影响
106
+
107
+ 不会直接使缓存失效;具名消费方负责请求前缀的任何变化。
108
+
109
+ ## 已知限制与延期工作
110
+
111
+ <a id="known-limitations-and-deferred-work"></a>
112
+
113
+
114
+ 这些限制说明该约定何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用文件系统对比或任务积压。
115
+
116
+ - **变更操作约定只支持文本**:文本读取和两个变更操作都以 `FS_NOT_TEXT` 拒绝二进制/非 UTF-8 内容;`readBytes` 与 `readByteRange` 是原始字节原语,二进制安全的变更操作仍延期。
117
+ - **没有复制**:移动只在同一后端内进行,绝不跨文件系统复制内容;`listDir` 仍然只列出一层,glob、分页与搜索不在范围内(见[目录列出笔记](../../../.agents/notes/archived/architecture/2026-07-03-filesystem-directory-listing-seam.md))。
118
+ - **没有 I/O deadline**:该 seam 不启动超时;取消只是每个原语上尽力而为的可选 `AbortSignal`(见[fs 能力族立场](../README.zh.md))。
119
+ - **先解析后操作使远程后端每次工具调用需要两次往返**:折叠或缓存解析由这种后端自行决定。
120
+
121
+ <a id="dev-note"></a>
122
+ ### 开发备注
123
+
124
+ <details>
125
+ <summary>维护者的工作上下文——点击展开</summary>
126
+
127
+ 无。
128
+
129
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import { Service } from "@qilin-agent/kylin";
2
+ import { HarnessError } from "@qilin-agent/llm";
3
+ //#region lib/types/types.js
4
+ /**
5
+ * Vocabulary for the filesystem Service Definition (`ctx.fs`): the opaque target/version
6
+ * identities, the metadata `stat` returns, the write-intent and outcome shapes, the
7
+ * literal-edit request/outcome, and the typed error taxonomy.
8
+ * @module @qilin-agent/fs/types
9
+ */
10
+ /**
11
+ * Brand a string as an {@link FsTargetKey}. For backend use only — a consumer
12
+ * never manufactures a key, it receives one from `resolve()`.
13
+ * @param key - the backend's raw key string (the local backend passes a realpath).
14
+ * @returns the same string, branded; no validation is performed.
15
+ */
16
+ function FsTargetKey(key) {
17
+ return key;
18
+ }
19
+ /**
20
+ * Brand a string as an {@link FsVersion}. For backend use only — a consumer
21
+ * never manufactures a version, it receives one from `stat`/write/edit outcomes.
22
+ * @param v - the backend's raw version string.
23
+ * @returns the same string, branded; no validation is performed.
24
+ */
25
+ function FsVersion(v) {
26
+ return v;
27
+ }
28
+ /**
29
+ * Typed filesystem error. Extends {@link HarnessError} so it carries a stable
30
+ * {@link FsErrorCode} and chains `cause`. `qilin-fs` owns this vocabulary so
31
+ * backends and the policy layer raise the same codes instead of each inventing
32
+ * message strings.
33
+ */
34
+ var FsError = class extends HarnessError {
35
+ code;
36
+ constructor(message, code, options) {
37
+ super(message, code, options);
38
+ this.code = code;
39
+ }
40
+ };
41
+ //#endregion
42
+ //#region lib/types/index.js
43
+ /**
44
+ * Filesystem Service Definition for one execution world. Backends own stable target
45
+ * identity, process paths and file URIs, containment, text reads, decoding,
46
+ * binary rejection, and atomic mutations. Read windows and
47
+ * observed-state policy stay in consumer and policy plugins; `editText`
48
+ * remains here so version check, literal match, and rewrite share one critical
49
+ * section.
50
+ * @module @qilin-agent/fs
51
+ */
52
+ /**
53
+ * Abstract filesystem provider. Targets must preserve identity across aliases;
54
+ * reads expose regular UTF-8 text or typed errors, listings are stable and
55
+ * content-free, and mutations are atomic. Optional guards add stale protection
56
+ * without changing the unguarded provider contract.
57
+ */
58
+ var FileSystem = class extends Service {
59
+ constructor(ctx) {
60
+ super(ctx, "fs");
61
+ }
62
+ /**
63
+ * Observe one file or a directory's direct entries in this provider's execution world.
64
+ * @param target - resolved file or directory, including an absent path to observe for creation.
65
+ * @param changed - invalidation callback; errors can be reported during or after initialization.
66
+ * @param signal - cancels watcher initialization; the caller closes an initialized watcher.
67
+ * @returns a promise resolving once observation is active, with an asynchronous close function.
68
+ * @throws when the provider does not support watching or cannot initialize the watcher.
69
+ */
70
+ watch(target, changed, signal) {
71
+ signal.throwIfAborted();
72
+ return Promise.reject(new FsError("Filesystem watching is not supported by this provider.", "FS_IO_ERROR"));
73
+ }
74
+ /**
75
+ * The sandbox mode this backend enforces on mutations BY DEFAULT, or
76
+ * `undefined` when it does not confine at all — the capability fact the tool
77
+ * layer reads to advertise the escalation fields honestly (mirrors
78
+ * `ShellExecutor.sandboxMode`). The base class and the bare local backend
79
+ * report `undefined`; a sandboxing backend (`@qilin-agent/fs-sandbox`)
80
+ * overrides it with the deployment default. A session override may make the
81
+ * effective mode narrower or wider, so strict escalation widening is checked
82
+ * per call rather than encoded in this default-relative fact.
83
+ * @returns the configured default mode of a sandboxing backend; `undefined`
84
+ * for a backend that never confines.
85
+ */
86
+ get sandboxMode() {}
87
+ /**
88
+ * Map an absolute path from the harness host into this filesystem's
89
+ * execution world when both paths identify the same file. The base provider
90
+ * exposes no mapping; host-backed or explicitly shared backends override it.
91
+ * @param hostPath - absolute path in the harness host filesystem.
92
+ * @returns the process path for the same file, or undefined when this
93
+ * execution world cannot read that host file.
94
+ */
95
+ processPathFromHostPath(hostPath) {}
96
+ };
97
+ //#endregion
98
+ export { FileSystem, FileSystem as default, FsError, FsTargetKey, FsVersion };
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Filesystem Service Definition for one execution world. Backends own stable target
3
+ * identity, process paths and file URIs, containment, text reads, decoding,
4
+ * binary rejection, and atomic mutations. Read windows and
5
+ * observed-state policy stay in consumer and policy plugins; `editText`
6
+ * remains here so version check, literal match, and rewrite share one critical
7
+ * section.
8
+ * @module @qilin-agent/fs
9
+ */
10
+ import { Context, Service } from '@qilin-agent/kylin';
11
+ import type { SandboxExecutionPolicy, SandboxMode } from '@qilin-agent/sandbox';
12
+ import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsObservation, FsMoveOutcome, FsRemoveOutcome, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from './types.ts';
13
+ export { FsError, FsTargetKey, FsVersion, } from './types.ts';
14
+ export type { FsEditOutcome, FsEditRequest, FsDirEntry, FsErrorCode, FsInfo, FsMoveOutcome, FsObservation, FsPathInfo, FsRemoveOutcome, FsTarget, FsWriteIntent, FsWriteOutcome, } from './types.ts';
15
+ declare module '@qilin-agent/kylin' {
16
+ interface Context {
17
+ fs: FileSystem;
18
+ }
19
+ interface Events {
20
+ /**
21
+ * Single-slot decision for the next {@link FileSystem.writeText}. Calling
22
+ * `next()` yields the bare provider's unconditional write; the first listener
23
+ * that returns an intent owns the decision rather than composing with peers.
24
+ * @param target - the resolved target about to be written.
25
+ * @param actor - the opaque tool-execution context the decider keys off.
26
+ * @mode waterfall
27
+ */
28
+ 'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>;
29
+ /**
30
+ * Single-slot decision for the next {@link FileSystem.editText}. Calling
31
+ * `next()` yields an unconditional edit; the first returned guard wins.
32
+ * @param target - the resolved target about to be edited.
33
+ * @param actor - the opaque tool-execution context the decider keys off.
34
+ * @mode waterfall
35
+ */
36
+ 'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => {
37
+ version: FsVersion;
38
+ } | undefined | Promise<{
39
+ version: FsVersion;
40
+ } | undefined>): Promise<{
41
+ version: FsVersion;
42
+ } | undefined>;
43
+ /**
44
+ * Record an authoritative positive or negative observation. Listeners must
45
+ * be synchronous recorders: throws fail the tool call and returned promises
46
+ * are not awaited.
47
+ * @param target - the target whose presence or absence was observed.
48
+ * @param observation - present with its version, or confirmed absent.
49
+ * @param actor - the observing tool-execution context; undefined records nothing useful.
50
+ * @mode emit
51
+ */
52
+ 'fs/observed'(target: FsTarget, observation: FsObservation, actor: object | undefined): void;
53
+ }
54
+ }
55
+ /**
56
+ * Abstract filesystem provider. Targets must preserve identity across aliases;
57
+ * reads expose regular UTF-8 text or typed errors, listings are stable and
58
+ * content-free, and mutations are atomic. Optional guards add stale protection
59
+ * without changing the unguarded provider contract.
60
+ */
61
+ export declare abstract class FileSystem extends Service {
62
+ constructor(ctx: Context);
63
+ /**
64
+ * Observe one file or a directory's direct entries in this provider's execution world.
65
+ * @param target - resolved file or directory, including an absent path to observe for creation.
66
+ * @param changed - invalidation callback; errors can be reported during or after initialization.
67
+ * @param signal - cancels watcher initialization; the caller closes an initialized watcher.
68
+ * @returns a promise resolving once observation is active, with an asynchronous close function.
69
+ * @throws when the provider does not support watching or cannot initialize the watcher.
70
+ */
71
+ watch(target: FsTarget, changed: (error?: Error) => void, signal: AbortSignal): Promise<() => Promise<void>>;
72
+ /**
73
+ * The sandbox mode this backend enforces on mutations BY DEFAULT, or
74
+ * `undefined` when it does not confine at all — the capability fact the tool
75
+ * layer reads to advertise the escalation fields honestly (mirrors
76
+ * `ShellExecutor.sandboxMode`). The base class and the bare local backend
77
+ * report `undefined`; a sandboxing backend (`@qilin-agent/fs-sandbox`)
78
+ * overrides it with the deployment default. A session override may make the
79
+ * effective mode narrower or wider, so strict escalation widening is checked
80
+ * per call rather than encoded in this default-relative fact.
81
+ * @returns the configured default mode of a sandboxing backend; `undefined`
82
+ * for a backend that never confines.
83
+ */
84
+ get sandboxMode(): SandboxMode | undefined;
85
+ /**
86
+ * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a
87
+ * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence
88
+ * async even though the local backend only normalizes + realpaths.
89
+ *
90
+ * @param path - the path to resolve; relative paths resolve against `opts.cwd`.
91
+ * @param opts - optional cwd override and cancellation signal.
92
+ * @returns the stable target; the same file yields the same `targetKey`.
93
+ */
94
+ abstract resolve(path: string, opts?: {
95
+ cwd?: string;
96
+ signal?: AbortSignal;
97
+ }): Promise<FsTarget>;
98
+ /**
99
+ * Return the canonical absolute path a subprocess in this filesystem's
100
+ * execution world can open. The path is deliberately separate from
101
+ * {@link FsTarget.targetKey}: consumers may pass this value to another OS
102
+ * capability, but must continue treating the target key as opaque.
103
+ * @param target - the resolved target whose process path is required.
104
+ * @returns an absolute path in the backend's execution world.
105
+ */
106
+ abstract processPath(target: FsTarget): string;
107
+ /**
108
+ * Map an absolute path from the harness host into this filesystem's
109
+ * execution world when both paths identify the same file. The base provider
110
+ * exposes no mapping; host-backed or explicitly shared backends override it.
111
+ * @param hostPath - absolute path in the harness host filesystem.
112
+ * @returns the process path for the same file, or undefined when this
113
+ * execution world cannot read that host file.
114
+ */
115
+ processPathFromHostPath(hostPath: string): string | undefined;
116
+ /**
117
+ * Return the canonical `file:` URI for a target in this filesystem's
118
+ * execution world. Backends own URI encoding because the host platform may
119
+ * differ from the execution platform.
120
+ * @param target - the resolved target to encode.
121
+ * @returns the target's canonical file URI.
122
+ */
123
+ abstract fileUrl(target: FsTarget): string;
124
+ /**
125
+ * Test canonical containment without exposing or parsing backend target
126
+ * keys. Both targets must come from this provider.
127
+ * @param parent - canonical directory target.
128
+ * @param child - canonical candidate target.
129
+ * @returns true when `child` is `parent` or a descendant of it.
130
+ */
131
+ abstract contains(parent: FsTarget, child: FsTarget): boolean;
132
+ /**
133
+ * Return target metadata, or `undefined` when the target does not exist.
134
+ * @param target - the resolved target to stat.
135
+ * @param signal - aborts the metadata round-trip.
136
+ * @returns metadata only, never content; undefined for an absent target.
137
+ */
138
+ abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
139
+ /**
140
+ * Return path metadata without following the final path component when it is a
141
+ * symbolic link. This is intentionally path-shaped, not target-shaped:
142
+ * {@link resolve} follows symlinks to produce the stable identity used by
143
+ * normal reads/writes, while `lstat` lets a consumer reject the path itself
144
+ * before that follow happens.
145
+ *
146
+ * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is
147
+ * absent.
148
+ * @param path - the path to inspect; relative paths resolve against `opts.cwd`.
149
+ * @param opts - `cwd` overrides the backend's default base for relative paths.
150
+ * @param signal - aborts the metadata round-trip.
151
+ * @returns metadata only, never content; undefined for an absent path.
152
+ */
153
+ abstract lstat(path: string, opts?: {
154
+ cwd?: string;
155
+ }, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
156
+ /**
157
+ * Read the whole regular text file as a single decoded string.
158
+ * @param target - the resolved target to read.
159
+ * @param signal - aborts the read.
160
+ * @returns the full decoded UTF-8 content.
161
+ */
162
+ abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>;
163
+ /**
164
+ * Stream the whole regular text file as decoded text chunks (same text
165
+ * semantics as {@link readText}, for large files). The backend owns
166
+ * cross-chunk UTF-8 decoding and binary rejection so the policy layer never
167
+ * touches raw bytes.
168
+ * @param target - the resolved target to read.
169
+ * @param signal - aborts the stream, including between chunks.
170
+ * @returns the chunk iterable, decoded and validated like {@link readText}.
171
+ */
172
+ abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>;
173
+ /**
174
+ * Read the whole regular file as raw bytes with no decoding or binary
175
+ * rejection. The bound lives at this seam so a backend can never buffer an
176
+ * unbounded file: a target known or discovered to exceed `maxBytes` fails
177
+ * with `FS_TOO_LARGE` instead of returning a truncated result.
178
+ * @param target - the resolved target to read.
179
+ * @param signal - aborts the read.
180
+ * @param maxBytes - inclusive byte cap on the complete content.
181
+ * @returns the full raw content, at most `maxBytes` long.
182
+ */
183
+ abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
184
+ /**
185
+ * Read one byte window of the regular file as raw bytes with no decoding or
186
+ * binary rejection: the bytes at `[offset, offset + length)`, shorter when
187
+ * the file ends inside the window and empty when `offset` lies at or past
188
+ * its end. The window is the bound here, not the file: a backend transfers
189
+ * at most `length` bytes of content beyond the prefix it skips to reach
190
+ * `offset` and never buffers the whole file, so the caller's cap on `length`
191
+ * is the guard against unbounded buffering.
192
+ * @param target - the resolved target to read.
193
+ * @param range - `offset`, the 0-based first byte, and `length`, the largest byte count; both non-negative integers.
194
+ * @param signal - aborts the read.
195
+ * @returns the window's bytes, at most `length` long.
196
+ */
197
+ abstract readByteRange(target: FsTarget, range: {
198
+ offset: number;
199
+ length: number;
200
+ }, signal?: AbortSignal): Promise<Uint8Array>;
201
+ /**
202
+ * List direct children of a directory in stable name order. Returns resolved
203
+ * child targets plus cheap metadata only; never reads file contents.
204
+ * @param target - the resolved directory target.
205
+ * @param signal - aborts the listing.
206
+ * @returns one entry per direct child, in stable name order.
207
+ */
208
+ abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>;
209
+ /**
210
+ * Atomically create or replace UTF-8 text. `expected` guards intent and
211
+ * staleness; omission allows unconditional overwrite.
212
+ * @param target - the resolved target to write.
213
+ * @param content - the full new file content.
214
+ * @param expected - the write intent guarding the write; omit for unconditional.
215
+ * @param signal - aborts before atomic publication takes effect.
216
+ * @param sandboxPolicy - the per-call mode and workspace root this write
217
+ * runs under; a sandboxing backend fences the write by it, the bare backend
218
+ * ignores it. Omit to leave the backend its own default.
219
+ * @returns the outcome, including the version the write produced.
220
+ */
221
+ abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsWriteOutcome>;
222
+ /**
223
+ * Atomically create or replace raw bytes. Same intent and staleness
224
+ * contract as {@link writeText}; the bytes are published as-is, so the
225
+ * caller owns the content encoding.
226
+ * @param target - the resolved target to write.
227
+ * @param content - the full new file content as raw bytes.
228
+ * @param expected - the write intent guarding the write; omit for unconditional.
229
+ * @param signal - aborts before atomic publication takes effect.
230
+ * @param sandboxPolicy - the per-call mode and workspace root this write
231
+ * runs under; a sandboxing backend fences the write by it, the bare backend
232
+ * ignores it. Omit to leave the backend its own default.
233
+ * @returns the outcome, including the version the write produced.
234
+ */
235
+ abstract writeBytes(target: FsTarget, content: Uint8Array, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsWriteOutcome>;
236
+ /**
237
+ * Atomically edit literal text. When supplied, the version guard is checked
238
+ * before matching so stale content reports `FS_STALE_VERSION`; omission edits
239
+ * the current content without a freshness precondition.
240
+ * @param target - the resolved target to edit.
241
+ * @param edit - the literal search/replace request.
242
+ * @param expected - the version guard; omit for an unconditional edit.
243
+ * @param signal - aborts before atomic publication takes effect.
244
+ * @param sandboxPolicy - the per-call mode and workspace root this edit runs
245
+ * under; a sandboxing backend fences the edit by it, the bare backend
246
+ * ignores it. Omit to leave the backend its own default.
247
+ * @returns the outcome, including the version the edit produced.
248
+ */
249
+ abstract editText(target: FsTarget, edit: FsEditRequest, expected?: {
250
+ version: FsVersion;
251
+ }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
252
+ /**
253
+ * Remove one file or directory. The operation acts on the target's stable
254
+ * identity, so a path reached through a final symbolic link removes what that
255
+ * link resolves to, exactly as {@link writeText} writes through it.
256
+ * @param target - the resolved target to remove.
257
+ * @param opts - `recursive` removes a directory with all its contents; when
258
+ * false, a non-empty directory fails with `FS_NOT_EMPTY`.
259
+ * @param signal - aborts before the removal takes effect.
260
+ * @param sandboxPolicy - the per-call mode and workspace root this removal
261
+ * runs under; a sandboxing backend fences the removal by it, the bare
262
+ * backend ignores it. Omit to leave the backend its own default.
263
+ * @returns whether a directory was removed; an absent target fails with `FS_NOT_FOUND`.
264
+ * @throws {FsError} with `FS_NOT_FOUND` for an absent target, `FS_NOT_EMPTY`
265
+ * for a non-empty directory removed without `recursive`, `FS_PERMISSION_DENIED`
266
+ * when the execution world refuses the removal, or `FS_IO_ERROR` for any
267
+ * other backend failure.
268
+ */
269
+ abstract remove(target: FsTarget, opts: {
270
+ recursive: boolean;
271
+ }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsRemoveOutcome>;
272
+ /**
273
+ * Move or rename within one backend's execution world. A move never crosses
274
+ * backends: both targets must come from this provider, and a backend that
275
+ * cannot move a target (for example across filesystems) fails with `FS_IO_ERROR`
276
+ * rather than copying. Like the other mutations, the operation acts on each
277
+ * target's stable identity.
278
+ * @param from - the resolved source target.
279
+ * @param to - the resolved destination target; its parent directory must exist.
280
+ * @param opts - `overwrite` replaces an existing file at the destination; with
281
+ * false an existing destination fails with `FS_EXISTS`, and a non-empty
282
+ * destination directory always fails with `FS_NOT_EMPTY`.
283
+ * @param signal - aborts before the move takes effect.
284
+ * @param sandboxPolicy - the per-call mode and workspace root this move runs
285
+ * under; a sandboxing backend fences both ends by it, the bare backend
286
+ * ignores it. Omit to leave the backend its own default.
287
+ * @returns the destination's observed version when the backend can report one,
288
+ * and whether an existing target was replaced.
289
+ * @throws {FsError} with `FS_NOT_FOUND` for an absent source or destination
290
+ * parent, `FS_EXISTS` for an existing destination moved onto without
291
+ * `overwrite`, `FS_NOT_EMPTY` for a non-empty destination directory,
292
+ * `FS_PERMISSION_DENIED` when the execution world refuses the move, or
293
+ * `FS_IO_ERROR` for any other backend failure.
294
+ */
295
+ abstract move(from: FsTarget, to: FsTarget, opts: {
296
+ overwrite: boolean;
297
+ }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsMoveOutcome>;
298
+ /**
299
+ * Create one directory. The operation acts on the target's stable identity and
300
+ * creates nothing outside the backend's own execution world.
301
+ * @param target - the resolved directory target to create.
302
+ * @param opts - `recursive` also creates missing parent directories; when
303
+ * false, a missing parent fails with `FS_NOT_FOUND`. Either way an existing
304
+ * directory is not an error only when `recursive` is true.
305
+ * @param signal - aborts before the directory is created.
306
+ * @param sandboxPolicy - the per-call mode and workspace root this creation
307
+ * runs under; a sandboxing backend fences it by it, the bare backend ignores
308
+ * it. Omit to leave the backend its own default.
309
+ * @returns nothing; the caller observes the new directory through `stat`/`listDir`.
310
+ * @throws {FsError} with `FS_EXISTS` for an existing entry the call would not
311
+ * accept, `FS_NOT_FOUND` for a missing parent without `recursive`,
312
+ * `FS_PERMISSION_DENIED` when the execution world refuses the creation, or
313
+ * `FS_IO_ERROR` for any other backend failure.
314
+ */
315
+ abstract createDirectory(target: FsTarget, opts: {
316
+ recursive: boolean;
317
+ }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<void>;
318
+ }
319
+ export default FileSystem;
320
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Vocabulary for the filesystem Service Definition (`ctx.fs`): the opaque target/version
3
+ * identities, the metadata `stat` returns, the write-intent and outcome shapes, the
4
+ * literal-edit request/outcome, and the typed error taxonomy.
5
+ * @module @qilin-agent/fs/types
6
+ */
7
+ import { HarnessError } from '@qilin-agent/llm';
8
+ import type { Branded } from '@qilin-agent/brand';
9
+ /**
10
+ * Opaque key for stale guards and target lookup. The local backend uses a
11
+ * realpath-like string; a remote backend might use a workspace URI or file id.
12
+ * Consumers MUST NOT parse it or assume it is a local absolute path.
13
+ */
14
+ export type FsTargetKey = Branded<'FsTargetKey'>;
15
+ /**
16
+ * Brand a string as an {@link FsTargetKey}. For backend use only — a consumer
17
+ * never manufactures a key, it receives one from `resolve()`.
18
+ * @param key - the backend's raw key string (the local backend passes a realpath).
19
+ * @returns the same string, branded; no validation is performed.
20
+ */
21
+ export declare function FsTargetKey(key: string): FsTargetKey;
22
+ /**
23
+ * Opaque file-version token — the freshness token a write/edit guards against.
24
+ * The local backend derives it from high-resolution stat identity and freshness
25
+ * fields; a remote backend might use a revision id. The policy layer records it
26
+ * for stale checks; consumers may display related metadata but MUST NOT
27
+ * interpret this token.
28
+ */
29
+ export type FsVersion = Branded<'FsVersion'>;
30
+ /**
31
+ * Brand a string as an {@link FsVersion}. For backend use only — a consumer
32
+ * never manufactures a version, it receives one from `stat`/write/edit outcomes.
33
+ * @param v - the backend's raw version string.
34
+ * @returns the same string, branded; no validation is performed.
35
+ */
36
+ export declare function FsVersion(v: string): FsVersion;
37
+ /**
38
+ * One authoritative observation of a target. A present observation carries the
39
+ * version used by guarded replacement; an absent observation authorizes only a
40
+ * guarded create, never an edit.
41
+ */
42
+ export type FsObservation = {
43
+ readonly kind: 'present';
44
+ readonly version: FsVersion;
45
+ } | {
46
+ readonly kind: 'absent';
47
+ };
48
+ /**
49
+ * A path resolved by a backend into a stable identity. `resolve()` produces
50
+ * this; every other operation takes it.
51
+ */
52
+ export interface FsTarget {
53
+ /** Opaque key for stale guards and target lookup. */
54
+ targetKey: FsTargetKey;
55
+ /**
56
+ * Path for model/UI-facing output. May be a local absolute path,
57
+ * workspace-relative path, or remote URI depending on the backend.
58
+ */
59
+ displayPath: string;
60
+ }
61
+ /**
62
+ * Metadata about a target — what {@link FileSystem.stat} returns. Lets the
63
+ * policy layer reject directories/special files before reading and choose
64
+ * `readText` vs `streamText` from `size` without probing by failure. `version`
65
+ * is the freshness token. `undefined` from `stat` means the target is absent.
66
+ */
67
+ export interface FsInfo {
68
+ /** Opaque freshness token of the target right now. */
69
+ version: FsVersion;
70
+ /** Whether the target is a regular file, a directory, or something else. */
71
+ type: 'file' | 'directory' | 'other';
72
+ /** Byte size of a regular file, when the backend can report it. */
73
+ size?: number;
74
+ }
75
+ /**
76
+ * Metadata about a path without following the final path component when it is a
77
+ * symbolic link. Unlike {@link FsInfo}, this path-level probe can report
78
+ * `symlink` so consumers with trust-boundary rules can reject repository-owned
79
+ * links before resolving a target.
80
+ */
81
+ export interface FsPathInfo {
82
+ /** Opaque freshness token of the path entry right now. */
83
+ version: FsVersion;
84
+ /** Whether the path entry is a regular file, directory, symlink, or other. */
85
+ type: 'file' | 'directory' | 'symlink' | 'other';
86
+ /** Byte size of the path entry, when the backend can report it. */
87
+ size?: number;
88
+ }
89
+ /**
90
+ * One direct child returned by {@link FileSystem.listDir}. Listing returns
91
+ * metadata and resolved targets only; it must not read file contents.
92
+ */
93
+ export interface FsDirEntry {
94
+ /** Basename of the child inside the listed directory. */
95
+ name: string;
96
+ /** Whether the child is a regular file, a directory, or something else. */
97
+ type: 'file' | 'directory' | 'other';
98
+ /** Resolved child target for follow-up operations. */
99
+ target: FsTarget;
100
+ /** Opaque freshness token when the backend can report metadata cheaply. */
101
+ version?: FsVersion;
102
+ /** Byte size of a regular file, when the backend can report it. */
103
+ size?: number;
104
+ /** Present when the listed name itself is a symbolic link; `type` still names what it resolves to. */
105
+ symlink?: boolean;
106
+ }
107
+ /**
108
+ * Guarded write intent. `createIfAbsent` rejects an existing target with
109
+ * `FS_NOT_OBSERVED`; `replaceIfVersion` rejects absence or mismatch with
110
+ * `FS_STALE_VERSION`. Omitting the intent from `writeText` means unconditional
111
+ * create-or-overwrite, not a third union arm.
112
+ */
113
+ export type FsWriteIntent = {
114
+ kind: 'createIfAbsent';
115
+ } | {
116
+ kind: 'replaceIfVersion';
117
+ version: FsVersion;
118
+ };
119
+ /** Outcome of a full-file write. */
120
+ export interface FsWriteOutcome {
121
+ /** Whether the write created a new file or replaced an existing one. */
122
+ operation: 'create' | 'update';
123
+ /** Opaque version of the file after the write. */
124
+ version: FsVersion;
125
+ /**
126
+ * The file's content BEFORE the write, or `null` when the file did not exist
127
+ * (a create) or the backend declined a contextual basis (for example, a
128
+ * binary/non-UTF-8 prior file or either overwrite side reaching its exclusive limit).
129
+ * LF-normalized storage text (the diff basis), never a diff — a consumer
130
+ * computes the result-time contextual diff from `before`/`after` when
131
+ * `before` is present, else falls back to a whole-file diff.
132
+ */
133
+ before: string | null;
134
+ /** The file's content AFTER the write, LF-normalized to share `before`'s diff basis; `null` for a raw-bytes write (no text basis). */
135
+ after: string | null;
136
+ }
137
+ /** A literal-replacement edit request. */
138
+ export interface FsEditRequest {
139
+ /** Literal non-empty text to replace. Must match exactly (after line-ending normalization). */
140
+ oldString: string;
141
+ /** Literal replacement text. An empty string deletes the matched text. */
142
+ newString: string;
143
+ /** Replace every match instead of requiring exactly one. */
144
+ replaceAll: boolean;
145
+ }
146
+ /** Outcome of a literal edit. */
147
+ export interface FsEditOutcome {
148
+ /** Opaque version of the file after the edit. */
149
+ version: FsVersion;
150
+ /**
151
+ * The file's content BEFORE the edit. Raw storage text (LF-normalized by the
152
+ * backend), never a diff — a consumer computes the result-time contextual diff
153
+ * (the applied hunk with context) from `before`/`after`.
154
+ */
155
+ before: string;
156
+ /** The file's content AFTER the edit. */
157
+ after: string;
158
+ }
159
+ /** Outcome of a delete. */
160
+ export interface FsRemoveOutcome {
161
+ /** Whether the removed target was a directory rather than a file or other entry. */
162
+ directory: boolean;
163
+ }
164
+ /** Outcome of a move or rename inside one backend. */
165
+ export interface FsMoveOutcome {
166
+ /**
167
+ * Opaque version of the target after the move, when the backend can observe
168
+ * the destination. Omitted when it cannot (a backend that only relays the
169
+ * move reports no version rather than a fabricated one).
170
+ */
171
+ version?: FsVersion;
172
+ /** Whether a target already existed at the destination and the move replaced it. */
173
+ replaced: boolean;
174
+ }
175
+ /**
176
+ * Stable, machine-routable codes for filesystem failures. Carried on
177
+ * {@link FsError}; the tool registry exposes `{ name, code }` on `isError`
178
+ * results so retry/permission/UI layers can branch without parsing messages.
179
+ */
180
+ export type FsErrorCode = 'FS_NOT_FOUND' | 'FS_NOT_DIRECTORY' | 'FS_NOT_TEXT' | 'FS_NOT_REGULAR_FILE' | 'FS_NOT_EMPTY' | 'FS_EXISTS' | 'FS_TOO_LARGE' | 'FS_PERMISSION_DENIED' | 'FS_SANDBOX_DENIED' | 'FS_IO_ERROR' | 'FS_STALE_VERSION' | 'FS_NOT_OBSERVED' | 'FS_AMBIGUOUS_EDIT' | 'FS_EDIT_NOT_FOUND' | 'FS_ABORTED';
181
+ /**
182
+ * Typed filesystem error. Extends {@link HarnessError} so it carries a stable
183
+ * {@link FsErrorCode} and chains `cause`. `qilin-fs` owns this vocabulary so
184
+ * backends and the policy layer raise the same codes instead of each inventing
185
+ * message strings.
186
+ */
187
+ export declare class FsError extends HarnessError {
188
+ readonly code: FsErrorCode;
189
+ constructor(message: string, code: FsErrorCode, options?: ErrorOptions);
190
+ }
191
+ //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@qilin-agent/fs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Abstract filesystem capability seam (ctx.fs) for the QiLin — vocabulary types, the FileSystem service (text IO + optional version-guarded atomic mutations), and the fs/* policy event vocabulary",
4
+ "version": "3.1.4",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kkutysllb/QiLin.git",
11
+ "directory": "packages/fs/fs"
12
+ },
13
+ "engines": {
14
+ "node": "^22.19.0 || >=24.0.0"
15
+ },
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "types": "lib/types/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/types/index.d.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./src/*": "./src/*",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/types/**/*.d.ts"
30
+ ],
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "@qilin-agent/brand": "3.1.4",
34
+ "@qilin-agent/sandbox": "3.1.4",
35
+ "@qilin-agent/llm": "3.1.4",
36
+ "@qilin-agent/kylin": "~4.0.3"
37
+ },
38
+ "devDependencies": {
39
+ "@qilin-agent/brand": "3.1.4",
40
+ "@qilin-agent/sandbox": "3.1.4",
41
+ "@qilin-agent/llm": "3.1.4",
42
+ "@qilin-agent/kylin": "~4.0.3"
43
+ }
6
44
  }