@openclaw/fs-safe 0.20.0 → 0.21.1
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/CHANGELOG.md +47 -0
- package/README.md +9 -1
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.js +1 -0
- package/dist/archive-zip-directory.js +4 -0
- package/dist/archive-zip-loader.js +3 -1
- package/dist/archive-zip-manifest.js +3 -1
- package/dist/atomic.d.ts +1 -1
- package/dist/copy-publication.d.ts +1 -3
- package/dist/copy-publication.js +2 -6
- package/dist/deny-mutation-match.d.ts +2 -0
- package/dist/deny-mutation-match.js +71 -0
- package/dist/deny-mutations.js +4 -3
- package/dist/file-identity.js +10 -2
- package/dist/file-lock-sync-admission.js +5 -1
- package/dist/file-lock-sync-root-io.d.ts +2 -2
- package/dist/file-lock-sync-root-io.js +1 -1
- package/dist/file-lock-sync.js +2 -2
- package/dist/file-store-prune.js +4 -4
- package/dist/mutation-authority.js +5 -0
- package/dist/native-binding.d.ts +33 -0
- package/dist/native-pinned-write.js +8 -2
- package/dist/pinned-mutation-admission.js +4 -2
- package/dist/replace-file-buffer.d.ts +4 -0
- package/dist/replace-file-buffer.js +36 -0
- package/dist/replace-file-copy-fallback.d.ts +2 -0
- package/dist/replace-file-copy-fallback.js +66 -38
- package/dist/replace-file-descriptor.d.ts +4 -0
- package/dist/replace-file-descriptor.js +9 -1
- package/dist/replace-file-destination.d.ts +21 -0
- package/dist/replace-file-destination.js +123 -0
- package/dist/replace-file-mutation.d.ts +26 -0
- package/dist/replace-file-mutation.js +47 -0
- package/dist/replace-file-temp-owner.d.ts +2 -2
- package/dist/replace-file-temp-owner.js +16 -4
- package/dist/replace-file-types.d.ts +55 -0
- package/dist/replace-file-types.js +1 -0
- package/dist/replace-file.d.ts +3 -55
- package/dist/replace-file.js +31 -11
- package/dist/retained-file-types.d.ts +50 -0
- package/dist/retained-file-types.js +1 -0
- package/dist/retained-file.d.ts +3 -0
- package/dist/retained-file.js +121 -0
- package/dist/root-directory-entry.d.ts +9 -0
- package/dist/root-directory-entry.js +28 -0
- package/dist/root-directory-list.d.ts +9 -1
- package/dist/root-directory-list.js +88 -37
- package/dist/root-handle-context.d.ts +4 -0
- package/dist/root-handle-context.js +12 -0
- package/dist/root-impl.d.ts +3 -3
- package/dist/root-impl.js +8 -2
- package/dist/root-move-noreplace.js +3 -3
- package/dist/root-walk.d.ts +19 -12
- package/dist/root-walk.js +49 -18
- package/dist/sidecar-lock-acquire.js +3 -3
- package/dist/sidecar-lock-reclaim.d.ts +2 -2
- package/dist/sidecar-lock-reclaim.js +6 -6
- package/dist/sidecar-lock.js +4 -4
- package/dist/staged-symlink-types.d.ts +4 -14
- package/dist/temp-target.js +3 -2
- package/dist/temp-workspace-admission.js +22 -21
- package/dist/temp-workspace-child-admission.d.ts +1 -1
- package/dist/temp-workspace-child-admission.js +14 -9
- package/dist/temp-workspace-ownership.d.ts +8 -0
- package/dist/temp-workspace-ownership.js +52 -0
- package/dist/test-hooks.d.ts +4 -0
- package/dist/watch-alias.d.ts +6 -0
- package/dist/watch-alias.js +88 -0
- package/dist/watch-hints.d.ts +9 -0
- package/dist/watch-hints.js +93 -0
- package/dist/watch-native.d.ts +36 -0
- package/dist/watch-native.js +73 -0
- package/dist/watch-scan.d.ts +28 -0
- package/dist/watch-scan.js +300 -0
- package/dist/watch-stream.d.ts +8 -0
- package/dist/watch-stream.js +32 -0
- package/dist/watch-types.d.ts +60 -0
- package/dist/watch-types.js +1 -0
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +530 -0
- package/docs/advanced.md +1 -0
- package/docs/archive.md +6 -0
- package/docs/atomic.md +82 -0
- package/docs/contributing.md +74 -6
- package/docs/durability.md +7 -0
- package/docs/index.md +1 -0
- package/docs/install.md +2 -0
- package/docs/native-helper.md +9 -0
- package/docs/native.md +24 -6
- package/docs/public-api.md +23 -2
- package/docs/retained-file.md +115 -0
- package/docs/root.md +25 -8
- package/docs/sidecar-lock.md +1 -1
- package/docs/staged-symlink.md +2 -1
- package/docs/temp.md +24 -4
- package/docs/testing.md +186 -4
- package/docs/types.md +6 -0
- package/docs/walk.md +22 -1
- package/docs/watch.md +251 -0
- package/package.json +12 -8
package/docs/contributing.md
CHANGED
|
@@ -94,11 +94,11 @@ GLIBC versions. To inspect an existing binding, use
|
|
|
94
94
|
`pnpm native:build` remains a host-toolchain development build; it does not
|
|
95
95
|
establish the GNU release ABI floor.
|
|
96
96
|
|
|
97
|
-
On Linux x64 with Docker, run `pnpm build`, copy the GNU
|
|
97
|
+
On Linux x64 or arm64 with Docker, run `pnpm build`, copy the matching GNU artifact from
|
|
98
98
|
`artifacts/` to `native/`, run `node scripts/stage-host-native.mjs`, then run
|
|
99
99
|
`bash scripts/test-linux-glibc-floor.sh`. CI uses this command to load the actual
|
|
100
100
|
artifact and run native security and no-replace move tests in Rocky Linux 8
|
|
101
|
-
(glibc 2.28). GNU
|
|
101
|
+
(glibc 2.28). Both GNU architectures execute this load test on matching runners.
|
|
102
102
|
|
|
103
103
|
## Test
|
|
104
104
|
|
|
@@ -167,6 +167,52 @@ pnpm check
|
|
|
167
167
|
This runs the filesystem boundary checks, build, tests, and package
|
|
168
168
|
tarball/import validation.
|
|
169
169
|
|
|
170
|
+
The native watch lane requires events on Linux, macOS, and Windows and reports
|
|
171
|
+
actual edit latency. Run `FS_SAFE_TEST_SERIAL=1 pnpm check` to isolate local timing
|
|
172
|
+
checks from the other filesystem stress suites. Watch fixtures use normal OS
|
|
173
|
+
temporary storage; session scratch trees may suppress macOS filesystem events.
|
|
174
|
+
|
|
175
|
+
### Optional Linux Testbox
|
|
176
|
+
|
|
177
|
+
The manual `testbox-validation.yml` workflow prepares a 16-vCPU Ubuntu 24.04
|
|
178
|
+
Blacksmith Testbox with Node 24.21.0, pnpm 12.4.2, dependencies, the Rust WASM
|
|
179
|
+
target, and the pinned portable archive compiler. It leaves library builds and
|
|
180
|
+
validation commands to the caller and does not replace required CI checks.
|
|
181
|
+
|
|
182
|
+
Use an authenticated Blacksmith CLI with access to the repository and its
|
|
183
|
+
Blacksmith organization. From a full repository checkout, warm one session
|
|
184
|
+
through Crabbox:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
CRABBOX_BLACKSMITH_IDLE_TIMEOUT=240m crabbox warmup --provider blacksmith-testbox \
|
|
188
|
+
--blacksmith-org openclaw \
|
|
189
|
+
--blacksmith-workflow .github/workflows/testbox-validation.yml \
|
|
190
|
+
--blacksmith-job validate --blacksmith-ref main \
|
|
191
|
+
--idle-timeout 240m --timing-json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use a branch or tag containing the workflow for `--blacksmith-ref`; GitHub must
|
|
195
|
+
first have registered the workflow on the default branch. The job has a fixed
|
|
196
|
+
240-minute limit. Keep the idle timeout at that limit (240 minutes in the native
|
|
197
|
+
Blacksmith CLI) because the pinned Testbox action can miss active SSH sessions
|
|
198
|
+
behind a forwarded port. Bound commands by the remaining job time and leave time
|
|
199
|
+
to collect results and stop before the deadline.
|
|
200
|
+
|
|
201
|
+
Use the returned `tbx_...` ID for subsequent commands. The `fs-safe-testbox`
|
|
202
|
+
wrapper restores the prepared tool paths and WASM compiler settings in the SSH
|
|
203
|
+
shell. For example:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
crabbox run --provider blacksmith-testbox --id <tbx_id> --timing-json -- \
|
|
207
|
+
fs-safe-testbox pnpm check
|
|
208
|
+
crabbox stop --provider blacksmith-testbox <tbx_id>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Stop the session when finished and verify its terminal status. Blacksmith owns
|
|
212
|
+
checkout synchronization; record the tested source revision or diff, Testbox ID,
|
|
213
|
+
and Actions run. This backend is Linux-only and does not accept Crabbox's direct
|
|
214
|
+
SSH `--script` or `--download` flags. The workflow provides no application secrets.
|
|
215
|
+
|
|
170
216
|
### Method benchmarks
|
|
171
217
|
|
|
172
218
|
`pnpm benchmark:methods` measures the callable library surface against synthetic
|
|
@@ -231,10 +277,32 @@ require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standa
|
|
|
231
277
|
direct `node` invocation without lifecycle metadata is unsupported. Archive
|
|
232
278
|
codecs and their dependencies are packed from the installed dependency graph.
|
|
233
279
|
|
|
234
|
-
PR CI builds and executes
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
280
|
+
PR CI builds and executes all seven shipped bindings. The existing check names
|
|
281
|
+
stay stable; extra runner/runtime combinations add checks without changing the
|
|
282
|
+
repository ruleset. Native lanes run Node 24; the GNU host lanes also exercise
|
|
283
|
+
Bun 1.4.2 (as do macOS and Windows).
|
|
284
|
+
|
|
285
|
+
| Lane | Runner | Architecture / libc | Runtime |
|
|
286
|
+
| --- | --- | --- | --- |
|
|
287
|
+
| JavaScript check | `ubuntu-latest` | x64 / glibc | Node 22, 24, 26 |
|
|
288
|
+
| JavaScript check | `macos-15` | arm64 | Node 22, 24, 26 |
|
|
289
|
+
| JavaScript check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 22, 24, 26 |
|
|
290
|
+
| Native check | `ubuntu-latest` | x64 / glibc | Node 24, Bun 1.4.2 |
|
|
291
|
+
| Native check (forced no-openat2) | `ubuntu-latest` | x64 / glibc | Node 24 |
|
|
292
|
+
| Native check | `ubuntu-24.04-arm` | arm64 / glibc | Node 24, Bun 1.4.2 |
|
|
293
|
+
| Native check | `macos-15` | arm64 | Node 24, Bun 1.4.2 |
|
|
294
|
+
| Native check | `macos-15-intel` | x64 | Node 24, Bun 1.4.2 |
|
|
295
|
+
| Native check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 24, Bun 1.4.2 |
|
|
296
|
+
| Native check | `windows-2022` (standard hosted) | x64 | Node 24, Bun 1.4.2 |
|
|
297
|
+
| Native check (Alpine 3.24) | `ubuntu-latest` | x64 / musl | Node 24 |
|
|
298
|
+
| Native check (Alpine 3.24) | `ubuntu-24.04-arm` | arm64 / musl | Node 24 |
|
|
299
|
+
| GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-latest` | x64 / glibc | Node 24 |
|
|
300
|
+
| GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-24.04-arm` | arm64 / glibc | Node 24 |
|
|
301
|
+
| Bundled package smoke | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22, 24 |
|
|
302
|
+
| Coverage | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22 |
|
|
303
|
+
|
|
304
|
+
Both musl lanes also run root-only package smoke with the real host binding.
|
|
305
|
+
The seven-target source build matrix still runs on release tags. The smoke writes manager versions,
|
|
238
306
|
cases, and synthetic-fixture scope to `release-artifacts/consumer-proof.json`.
|
|
239
307
|
|
|
240
308
|
## Docs
|
package/docs/durability.md
CHANGED
|
@@ -436,3 +436,10 @@ owning product boundary.
|
|
|
436
436
|
- [Migrating to 0.5](migrating-to-0.5.md) — choosing a publication policy during upgrade.
|
|
437
437
|
- [Native architecture](native.md) — clone/copy/hash mechanisms and fallback guarantees.
|
|
438
438
|
- [Errors](errors.md) — typed operational failure handling.
|
|
439
|
+
|
|
440
|
+
## Existing Windows file retirement
|
|
441
|
+
|
|
442
|
+
[`retainFileInDirectory`](retained-file.md) describes identity-bound native
|
|
443
|
+
disposition and resource settlement separately from persistence. Its result is
|
|
444
|
+
always `persistence: "not-proven"`; neither accepted disposition nor observed
|
|
445
|
+
namespace absence is a directory/volume barrier or an application commit.
|
package/docs/index.md
CHANGED
|
@@ -65,6 +65,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
65
65
|
| [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for private JSON/text state at 0600 under 0700 dirs. |
|
|
66
66
|
| [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
|
|
67
67
|
| [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
|
|
68
|
+
| [`watch`](watch.md) | Guarded filesystem observation with advisory native hints and polling. |
|
|
68
69
|
| [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
|
|
69
70
|
| [`Root.entries`](entries.md) | Guarded nonrecursive entries, bounded name collection, and caller-owned symlink validation. |
|
|
70
71
|
| [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
|
package/docs/install.md
CHANGED
|
@@ -130,6 +130,8 @@ features, including strict owned-tree temp cleanup, retained-directory staging,
|
|
|
130
130
|
and atomic `rename-noreplace` (including the default no-clobber `Root.move()`),
|
|
131
131
|
remain unavailable. Operations without a safe fallback fail with `helper-unavailable`
|
|
132
132
|
when the matching package is absent, incompatible, or disabled.
|
|
133
|
+
No-clobber `Root.move()` preserves a native loader failure in the error's
|
|
134
|
+
`cause`, including the original missing-library or incompatible-glibc diagnostic.
|
|
133
135
|
|
|
134
136
|
Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
|
|
135
137
|
before deploying with native mode `require` or native-only features.
|
package/docs/native-helper.md
CHANGED
|
@@ -182,3 +182,12 @@ consumer performs its 0.5 upgrade.
|
|
|
182
182
|
- [Durability](durability.md)
|
|
183
183
|
- [Migrating to 0.5](migrating-to-0.5.md)
|
|
184
184
|
- [Migrating to 0.6](migrating-to-0.6.md)
|
|
185
|
+
|
|
186
|
+
### Retained existing Windows file
|
|
187
|
+
|
|
188
|
+
The maintained Windows helper supports the public
|
|
189
|
+
[`retainFileInDirectory`](retained-file.md) lifecycle on fixed local NTFS.
|
|
190
|
+
Private native handles, exact identity/generation checks, writable-section
|
|
191
|
+
admission and explicit close results stay behind that public API. Older helpers
|
|
192
|
+
without this capability are unsupported; there is no pathname deletion fallback.
|
|
193
|
+
No Windows namespace persistence barrier is provided.
|
package/docs/native.md
CHANGED
|
@@ -96,12 +96,30 @@ operation is still a permission error and never triggers a retry. Install
|
|
|
96
96
|
syscall filters before the first native operation; a later `ENOSYS` fails with
|
|
97
97
|
`ENOTSUP`, rather than changing the cached mechanism during a call.
|
|
98
98
|
|
|
99
|
-
The fallback
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
The fallback inspects components with `O_PATH | O_NOFOLLOW`, follows relative
|
|
100
|
+
symlink targets using a stack of retained directory descriptors, and rejects
|
|
101
|
+
absolute targets or `..` past the retained root with `EXDEV`. It permits at most
|
|
102
|
+
40 link expansions (`ELOOP` beyond that limit). Final `O_NOFOLLOW` and exclusive
|
|
103
|
+
creation retain their syscall semantics, including opening the link itself with
|
|
104
|
+
`O_PATH | O_NOFOLLOW` and creating through a dangling in-root relative link.
|
|
105
|
+
Exact device/inode/type identities are checked before and after the final
|
|
106
|
+
no-follow open; followed links also retain their descriptors and have their
|
|
107
|
+
named identities and target strings rechecked. Detected replacements fail with
|
|
108
|
+
`EXDEV`, including changes to directories left behind by a link's `..` target.
|
|
109
|
+
|
|
110
|
+
The fallback also honors `nosymfollow` mount restrictions on retained links.
|
|
111
|
+
Two conservative restrictions remain: the fallback refuses to follow **any
|
|
112
|
+
procfs symlink** with `ELOOP`, including ordinary links such as `/proc/mounts`.
|
|
113
|
+
Userspace metadata cannot distinguish these from procfs magic links, which
|
|
114
|
+
`RESOLVE_NO_MAGICLINKS` must never follow. Opening a final link itself with
|
|
115
|
+
`O_PATH | O_NOFOLLOW` remains allowed. In a sticky, world-writable directory,
|
|
116
|
+
the fallback refuses all symlink following with `EACCES`, even when the calling
|
|
117
|
+
thread or directory owner owns the link, or `fs.protected_symlinks=0`. This
|
|
118
|
+
preserves Linux's protected-symlink restriction without assuming the thread's
|
|
119
|
+
filesystem UID or treating equal mapped `stat` UIDs as proof of equal kernel
|
|
120
|
+
owners (distinct unmapped owners can both appear as the overflow UID).
|
|
121
|
+
|
|
122
|
+
Public Root policy and canonical-path admission, hardlink rejection, pinned-file checks, and mutation
|
|
105
123
|
identity fences remain in place. `openBeneath()` reports `best-effort`: these
|
|
106
124
|
identity samples detect replacements but cannot make a multi-component walk
|
|
107
125
|
atomic against a hostile process renaming directories between samples. This
|
package/docs/public-api.md
CHANGED
|
@@ -79,6 +79,16 @@ missing relative suffixes beneath an existing directory using bounded temporary
|
|
|
79
79
|
directory probes. The caller owns Unicode-pair policy, caching, and the fallback
|
|
80
80
|
for `undefined`. See [path suffix alias probing](path-suffix-aliases.md).
|
|
81
81
|
|
|
82
|
+
`retainSymlinkInDirectory` holds an explicitly identified POSIX symlink through
|
|
83
|
+
exact-slot no-replace publication; its receipts use the `StagedSymlink*` and
|
|
84
|
+
`PublishedSymlinkReceipt` types. See [staged symlinks](staged-symlink.md).
|
|
85
|
+
|
|
86
|
+
`retainFileInDirectory` retains an existing Windows NTFS file through a native
|
|
87
|
+
handle for explicit identity-bound retirement. It is described by
|
|
88
|
+
`RetainFileInDirectoryOptions`, `RetainedFile`, `RetainedFileAdmission`,
|
|
89
|
+
`RetainedFileExpected`, `RetainedFileIssue`, `RetainedFileReceipt`, and
|
|
90
|
+
`RetainedFileResult`. See [retained Windows files](retained-file.md).
|
|
91
|
+
|
|
82
92
|
## Guest source
|
|
83
93
|
|
|
84
94
|
`@openclaw/fs-safe/guest` exports `GUEST_FILESYSTEM_PYTHON`,
|
|
@@ -132,8 +142,11 @@ parent/workspace descriptors; see the
|
|
|
132
142
|
Atomic helper option and receipt types include
|
|
133
143
|
`MovePathWithCopyFallbackOptions`, `ReplaceDirectoryAtomicOptions`,
|
|
134
144
|
`ReplaceFileAtomicSyncOptions`, `ReplaceFileAtomicResult`,
|
|
135
|
-
`ReplaceFileAtomicRestoreCleanup`, `ReplaceFileCopyFallbackRestorePolicy`,
|
|
136
|
-
`ReplaceFileDestinationHardlinkPolicy`.
|
|
145
|
+
`ReplaceFileAtomicRestoreCleanup`, `ReplaceFileCopyFallbackRestorePolicy`,
|
|
146
|
+
`ReplaceFileDestinationHardlinkPolicy`, and `ReplaceFileAtomicDestinationState`.
|
|
147
|
+
The `assertBeforeMutation` option rechecks caller authority before new effects,
|
|
148
|
+
and `onDestinationState` reports retained destination identities as
|
|
149
|
+
`ReplaceFileAtomicDestinationState` values; see [atomic writes](atomic.md).
|
|
137
150
|
|
|
138
151
|
The durability surface also exports the synchronous strict
|
|
139
152
|
`syncDirectorySync`, plus `DirectoryReceipt`, `DurableDirectoryReceipt`,
|
|
@@ -160,6 +173,14 @@ Archive option and policy types are `ExtractArchiveOptions`,
|
|
|
160
173
|
used by extractors. `resolvePackedRootDir` finds the single packed root when an
|
|
161
174
|
archive layout permits it; neither helper weakens entry validation.
|
|
162
175
|
|
|
176
|
+
## `watch`
|
|
177
|
+
|
|
178
|
+
`@openclaw/fs-safe/watch` exports `watch()` plus `WatchScope`, `WatchEntry`,
|
|
179
|
+
`WatchChange`, `WatchInvalidation`, `WatchFailure`, `WatchHealth`,
|
|
180
|
+
`WatchOptions`, and `WatchSubscription`. Invalidations are advisory; guarded
|
|
181
|
+
scans stay authoritative. See [filesystem observation](watch.md) for modes,
|
|
182
|
+
budgets, `persistent`, and lifecycle.
|
|
183
|
+
|
|
163
184
|
## Keeping this list honest
|
|
164
185
|
|
|
165
186
|
Every runtime and type name in `test/public-api.json` must appear somewhere in
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Retained Windows files
|
|
3
|
+
description: "Explicit identity-bound retirement of an existing file; resource settlement is not persistence."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retain an existing Windows file
|
|
7
|
+
|
|
8
|
+
`retainFileInDirectory` from `@openclaw/fs-safe/advanced` retains **one existing
|
|
9
|
+
regular direct child**, without creating or deleting anything at admission.
|
|
10
|
+
It requires the matching native package. There is no pathname-unlink fallback.
|
|
11
|
+
The initial implementation supports fixed local NTFS drives only; other systems
|
|
12
|
+
return `unsupported`.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { retainFileInDirectory } from "@openclaw/fs-safe/advanced";
|
|
16
|
+
|
|
17
|
+
const admission = retainFileInDirectory({
|
|
18
|
+
directory: producer.directory, // canonical C:\... spelling
|
|
19
|
+
parent: producer.parentIdentity, // exact bigint dev/ino
|
|
20
|
+
basename: producer.basename,
|
|
21
|
+
expected: producer.expected, // bigint dev/ino/size/mtimeNs/ctimeNs and SHA-256
|
|
22
|
+
assertBeforeMutation: () => producer.assertCurrentExclusiveAuthority(),
|
|
23
|
+
});
|
|
24
|
+
if (admission.status === "retained") {
|
|
25
|
+
using file = admission.file;
|
|
26
|
+
const outcome = file.remove(); // explicit; using alone does NOT delete
|
|
27
|
+
// outcome.persistence is always "not-proven".
|
|
28
|
+
producer.recordRetirementObservation(outcome);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The producer must capture its expected identity, generation and bytes during its
|
|
33
|
+
own creation/ownership protocol. Reading an arbitrary current pathname immediately
|
|
34
|
+
before admission does **not** authenticate the producer. `Number` identities,
|
|
35
|
+
zero/unknown identities, negative/overflowing identities, and malformed digests
|
|
36
|
+
are rejected. A full opaque native volume/file ID is included in the receipt;
|
|
37
|
+
no native handle or numeric descriptor is exposed. Receipts are immutable facts,
|
|
38
|
+
not transferable authority.
|
|
39
|
+
|
|
40
|
+
## Public types
|
|
41
|
+
|
|
42
|
+
- `RetainFileInDirectoryOptions`: immutable admission inputs and synchronous authority.
|
|
43
|
+
- `RetainedFileExpected`: exact producer identity, write generation and expected digest.
|
|
44
|
+
- `RetainedFileAdmission`: a retained `RetainedFile` or a refusal/settlement result.
|
|
45
|
+
- `RetainedFileReceipt`: immutable admitted facts, never reusable authority.
|
|
46
|
+
- `RetainedFileResult`: separate disposition, namespace, resources and persistence facts.
|
|
47
|
+
- `RetainedFileIssue`: phase, native code/message and optional original authority cause.
|
|
48
|
+
|
|
49
|
+
## Admission and mutation custody
|
|
50
|
+
|
|
51
|
+
The directory's ancestry is opened component by component without following
|
|
52
|
+
reparse points, with delete sharing denied. Its exact expected parent identity
|
|
53
|
+
and canonical path are checked. The file is opened relative to that retained
|
|
54
|
+
parent, with write/delete sharing denied. Existing writer handles refuse
|
|
55
|
+
admission; a read-oplock grant also excludes preexisting writable mapped
|
|
56
|
+
sections. That oplock request is cancelled and joined before admission returns,
|
|
57
|
+
while the no-write-sharing file handle remains open. No detached request remains.
|
|
58
|
+
|
|
59
|
+
The opened file must match the expected exact NTFS identity, single-link regular
|
|
60
|
+
type, write/change generation, size and SHA-256. Names with stream/device syntax,
|
|
61
|
+
trailing-dot/space aliases and observed named data streams are unsupported.
|
|
62
|
+
Readonly attributes and ACL denials are not repaired or overridden. Verification
|
|
63
|
+
is synchronous and bounded by `maxBytes` (default 16 MiB, maximum 64 MiB).
|
|
64
|
+
|
|
65
|
+
The original producer must retain **exclusive mutation custody for the entire
|
|
66
|
+
file**, including alternate streams, attributes, security and hardlink creation.
|
|
67
|
+
Windows read/write sharing is per-stream; it is not an application lock over
|
|
68
|
+
all possible aliases. The helper rejects observed alternate streams and changed
|
|
69
|
+
generations, but does not turn a point-in-time check into exclusion of concurrent
|
|
70
|
+
alias/attribute operations. The synchronous `assertBeforeMutation` must check
|
|
71
|
+
that this original authority is still current. Callers unable to establish that
|
|
72
|
+
custody must not call `remove`. Privileged/raw-volume/kernel modifications are
|
|
73
|
+
outside this capability's threat model.
|
|
74
|
+
|
|
75
|
+
`remove()` admits authority once, revalidates the retained object, sets native
|
|
76
|
+
handle disposition, closes the file, observes the name under the still-retained
|
|
77
|
+
parent, then closes ancestry. No pathname is passed to unlink. Ordinary
|
|
78
|
+
`dispose()` and `[Symbol.dispose]()` only close resources. Authority callbacks
|
|
79
|
+
returning Promises, thenables, or synchronous or asynchronous generator objects
|
|
80
|
+
are refused without deletion; generators are never advanced. Reentrancy is also
|
|
81
|
+
refused, and even caught reentrancy poisons that attempt.
|
|
82
|
+
Repeated settlement returns the original receipt without another mutation or
|
|
83
|
+
another authority call. A copied receipt cannot be used to remove a replacement.
|
|
84
|
+
`[Symbol.dispose]()` throws `FsSafeError` with the complete result in
|
|
85
|
+
`error.details.result` if resource settlement is uncertain; `dispose()` returns
|
|
86
|
+
that result directly. GC closes only and produces no settlement receipt; use
|
|
87
|
+
explicit disposal.
|
|
88
|
+
|
|
89
|
+
## Read the facts separately
|
|
90
|
+
|
|
91
|
+
| Field | Meaning |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `status` | `unsupported`, `not-attempted`, `preserved-mismatch`, `disposition-accepted`, `name-absent-after-settlement`, `failed`, or `indeterminate`. |
|
|
94
|
+
| `disposition` | Whether native deletion was unattempted, accepted, rejected, or indeterminate. |
|
|
95
|
+
| `namespace` | A post-file-close observation: absent, original, foreign, unknown, or not observed. Absence alone never authenticates prior deletion. |
|
|
96
|
+
| `resources` | `closed` or `close-failed`; operation and close errors are retained together. An uncertain close is never retried against a possibly recycled handle. |
|
|
97
|
+
| `persistence` | Always `not-proven`; no namespace persistence barrier was performed. |
|
|
98
|
+
|
|
99
|
+
An accepted disposition can still have an unknown/foreign/original namespace
|
|
100
|
+
observation or failed close. A later replacement may exist even after observed
|
|
101
|
+
absence. Existing read handles can retain access to old bytes after the name is
|
|
102
|
+
absent. Namespace observation does not certify all foreign handles are closed.
|
|
103
|
+
An unexpected native binding failure is indeterminate, not success.
|
|
104
|
+
|
|
105
|
+
## No persistence or service-transaction guarantee
|
|
106
|
+
|
|
107
|
+
This API does not flush a volume, implement a journal, certify power-loss-safe
|
|
108
|
+
unlink, or issue an application dependency release. Process-termination tests
|
|
109
|
+
establish process-owned handle lifetime only, not crash/storage durability.
|
|
110
|
+
|
|
111
|
+
An update transaction must independently qualify either a real namespace
|
|
112
|
+
persistence barrier or original-owned durable recovery with correct ordering,
|
|
113
|
+
generation binding and restart consumption. Until then it must retain its
|
|
114
|
+
uncertain recovery/dependency state. Successful removal of one payload does not
|
|
115
|
+
establish a committed multi-file transaction or justify deleting its receipt.
|
package/docs/root.md
CHANGED
|
@@ -68,7 +68,10 @@ fs.walk(rel, options) // root-bounded AsyncIterable<{ relativePath, kin
|
|
|
68
68
|
|
|
69
69
|
`walk()` is the incremental, root-bounded recursive scan. It supports entry and
|
|
70
70
|
depth budgets, cancellation, and `symlinkPolicy: "skip" |
|
|
71
|
-
"follow-within-root"
|
|
71
|
+
"follow-within-root" | "include"`. Include mode returns links as
|
|
72
|
+
`{ relativePath, kind: "symlink", size }` without resolving or entering their
|
|
73
|
+
targets, including dangling and outside-root links. `size` describes the link,
|
|
74
|
+
not its target. With an entry budget, sorted walks prepare small metadata
|
|
72
75
|
batches within the remaining budget; unbounded sorted walks reuse the full
|
|
73
76
|
directory snapshot.
|
|
74
77
|
The default `order: "sorted"` enumerates and sorts each directory's names;
|
|
@@ -92,6 +95,8 @@ Directory reads remain fail-fast by default. With
|
|
|
92
95
|
`{ relativePath, kind: "directory-error", size: 0, error }` and continues with
|
|
93
96
|
the remaining tree. That policy also covers identity-check failures after an
|
|
94
97
|
awaited filter, while callback failures always reject.
|
|
98
|
+
In include mode, a directory that becomes a symlink before descent is a
|
|
99
|
+
`path-mismatch` directory error; it is never silently omitted or followed.
|
|
95
100
|
See [Directory walking](walk.md) for the pure-Node guarantees and the contrast
|
|
96
101
|
with the standalone best-effort walkers.
|
|
97
102
|
|
|
@@ -258,8 +263,10 @@ occurred, later cancellation or verification failure preserves the destination.
|
|
|
258
263
|
The synchronous optional `onDestinationPublished` callback receives a frozen
|
|
259
264
|
`RootCopyPublicationReceipt` containing `{ path, dev, ino }`, with exact bigint identity immediately after
|
|
260
265
|
publication, before later checks can fail. Callback errors also preserve the
|
|
261
|
-
published file.
|
|
262
|
-
|
|
266
|
+
published file. Promise, thenable, and synchronous or asynchronous generator
|
|
267
|
+
results reject with `TypeError`; returned generators are never advanced. Other
|
|
268
|
+
synchronous return values are ignored. This receipt records an outcome; it does
|
|
269
|
+
not authorize removing a file that another actor may have edited. Application recovery and cooperative
|
|
263
270
|
locking remain caller-owned.
|
|
264
271
|
|
|
265
272
|
Existing `copyIn` callers must account for completed destinations retained after
|
|
@@ -319,10 +326,12 @@ truncation, append, move, and removal. Buffered writes use bounded chunks and
|
|
|
319
326
|
recheck before every partial-write submission; file removal submits a direct
|
|
320
327
|
unlink request. Native calls that perform multiple filesystem steps are one
|
|
321
328
|
dispatch. No asynchronous wait separates the check
|
|
322
|
-
from that dispatch. A thrown value rejects the operation unchanged;
|
|
323
|
-
or
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
from that dispatch. A thrown value rejects the operation unchanged; a Promise,
|
|
330
|
+
thenable, or synchronous or asynchronous generator result rejects with `TypeError`
|
|
331
|
+
before that mutation. Returned generators are never advanced. Other synchronous
|
|
332
|
+
return values are ignored. Generator detection applies to generator objects
|
|
333
|
+
themselves, not proxy wrappers. Callbacks can run multiple times and must inspect
|
|
334
|
+
current authority during each call; return-value validation does not establish it.
|
|
326
335
|
Directory creation rechecks the retained parent after the callback and before
|
|
327
336
|
submitting mkdir, so a replacement is rejected before creating that component.
|
|
328
337
|
Overwrite moves recheck the retained root, parents, source identity and both
|
|
@@ -349,7 +358,9 @@ For `openWritable()`, the callback covers the library's parent creation,
|
|
|
349
358
|
exclusive creation, and truncation. The returned raw `FileHandle` belongs to
|
|
350
359
|
the caller, which must check authority before its own later writes.
|
|
351
360
|
|
|
352
|
-
All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied.
|
|
361
|
+
All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Missing suffixes also match prospective case and canonical Unicode normalization aliases: case-folding and Unicode NFC/NFD-equivalent spellings cannot bypass a denied path or prefix. A read-only observation of the existing parent can establish that ASCII case variants are distinct. Admission never creates temporary probe files or directories. If sensitivity cannot be established (including empty or unreadable parents, future directories, and Unicode normalization), equivalent suffixes are denied conservatively, even on a filesystem that would allow distinct names. Distinct existing canonical ancestors and unrelated names remain distinct. Observations are local to each synchronous policy check and are refreshed after callbacks or mutations. This conservative fallback does not model other filesystem-specific equivalences, such as HFS+ ignorable formatting characters.
|
|
362
|
+
|
|
363
|
+
Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
|
|
353
364
|
|
|
354
365
|
`move()` snapshots its merged default and per-call mutation policy before
|
|
355
366
|
asynchronous preparation. Later changes to the original policy objects or arrays
|
|
@@ -384,6 +395,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
|
|
|
384
395
|
fs.resolve(rel) // absolute path inside the root, after canonicalization
|
|
385
396
|
```
|
|
386
397
|
|
|
398
|
+
Directory enumeration (`list`, `entries`, and `walk`) requires UTF-8 filenames.
|
|
399
|
+
A discovered name that cannot be represented losslessly as a JavaScript string
|
|
400
|
+
rejects with `invalid-path`, before looking up metadata through that name.
|
|
401
|
+
Streaming traversal may already have yielded earlier entries. Literal Unicode
|
|
402
|
+
replacement characters (`U+FFFD`) remain valid names.
|
|
403
|
+
|
|
387
404
|
These do not pin a later operation. During `stat()`, the exact selected target and
|
|
388
405
|
parent are checked around metadata collection; `list()` checks one exact selected
|
|
389
406
|
directory around the complete name/metadata batch instead of repeating containment
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -61,7 +61,7 @@ Always release locks in a `finally` block. Application-managed graceful shutdown
|
|
|
61
61
|
|
|
62
62
|
Exit cleanup tolerates shared managers created by older package copies that lack reclaim-guard state, and continues through later manager domains. Handler ownership remains first-registration-wins: loading an updated copy does not replace an older copy's registered handler. First registration is committed only after Node accepts the listener; synchronous `newListener` reentry fails closed and a thrown registration rolls back so a later acquisition can retry. Restart with the updated copy registering first to obtain the fix. After building, `node scripts/legacy-lock-exit-proof.mjs` checks clean exit, removal of legacy and modern raw locks, and preservation of a retained lock using synthetic temporary files.
|
|
63
63
|
|
|
64
|
-
Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode. Release compares the in-memory token and exact serialized bytes, and requires the pathname to remain a regular file, instead of requiring an opened descriptor and pathname lookup to report the same inode identity. This preserves ownership checks on filesystems such as Docker Desktop VirtioFS where those two views can legitimately differ. Sidecars created by older releases have no token and retain the legacy identity-plus-content check. If Windows reports a zero device or inode for either identity observation, that check is inconclusive and removal is skipped.
|
|
64
|
+
Each new sidecar also carries an internal random ownership token encoded as JSON trailing whitespace. `JSON.parse()` and every payload callback still see exactly the caller-provided object. Only the process that successfully created the sidecar keeps that token as release authority; merely reading token-shaped bytes from disk does not enable this mode. Release compares the in-memory token and exact serialized bytes, and requires the pathname to remain a regular file, instead of requiring an opened descriptor and pathname lookup to report the same inode identity. This preserves ownership checks on filesystems such as Docker Desktop VirtioFS where those two views can legitimately differ. Sidecars created by older releases have no token and retain the legacy identity-plus-content check. Raw acquisition, snapshot, and exit cleanup capture bigint device/inode values; unsafe numeric identities from older in-process receipts cannot authorize cleanup. If Windows reports a zero device or inode for either identity observation, that check is inconclusive and removal is skipped.
|
|
65
65
|
|
|
66
66
|
Last-chance raw-lock exit cleanup requires known Windows device and inode values from both pathname observations and the opened descriptor. A zero value leaves the sidecar in place, including for token-owned locks. Known descriptor/path identity differences remain supported; a pathname identity change during the read still prevents cleanup.
|
|
67
67
|
|
package/docs/staged-symlink.md
CHANGED
|
@@ -86,7 +86,8 @@ the original receipt is never replaced by a post-publication identity.
|
|
|
86
86
|
touching descriptor numbers.
|
|
87
87
|
- `await using` invokes cleanup and raises on a `preserved` outcome. Invocation
|
|
88
88
|
order serializes descriptor work; reentrant calls from the authority callback
|
|
89
|
-
reject. Callbacks must be synchronous; returned thenables
|
|
89
|
+
reject. Callbacks must be synchronous; returned thenables and synchronous or
|
|
90
|
+
asynchronous generator objects are refused. Generators are never advanced.
|
|
90
91
|
|
|
91
92
|
Errors carry `StagedSymlinkFailureDetails` in `FsSafeError.details`: the phase,
|
|
92
93
|
recorded `publication`, and a cleanup receipt when applicable. Publication is
|
package/docs/temp.md
CHANGED
|
@@ -22,10 +22,11 @@ substitutions; the compatible JavaScript fallback has the narrower race
|
|
|
22
22
|
contract documented below.
|
|
23
23
|
|
|
24
24
|
On POSIX, workspace creation verifies the supplied root and its canonical
|
|
25
|
-
ancestors before creating a child.
|
|
26
|
-
effective user
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
ancestors before creating a child. The supplied root must be owned by the
|
|
26
|
+
effective user and must not be group/world writable, even with the sticky bit.
|
|
27
|
+
Use a private per-user directory rather than supplying a shared `/tmp` directly.
|
|
28
|
+
Ancestors must be owned by the effective user or root; group/world-writable
|
|
29
|
+
ancestors must have the sticky bit. Foreign-owned directories and non-sticky writable ancestors reject
|
|
29
30
|
with `not-owned` or `insecure-permissions`; unavailable effective-user identity
|
|
30
31
|
rejects with `permission-unverified`. Existing supplied directories keep their
|
|
31
32
|
permissions. Missing root components are created at `0o700` and initialized
|
|
@@ -34,6 +35,25 @@ mode, correction uses a verified directory descriptor.
|
|
|
34
35
|
An initial mode-descriptor admission error is preserved if closing that rejected
|
|
35
36
|
descriptor also fails; close failures after successful admission remain reportable.
|
|
36
37
|
|
|
38
|
+
On Linux, non-identity `/proc/self/uid_map` and `/proc/self/gid_map` evidence
|
|
39
|
+
permits ancestors whose UID and GID equal unmapped kernel overflow IDs,
|
|
40
|
+
provided the same ancestor mode checks pass.
|
|
41
|
+
These owners are classified as **unmapped**, not verified root owners:
|
|
42
|
+
[Linux maps all unmapped owners to overflow IDs](https://man7.org/linux/man-pages/man7/user_namespaces.7.html).
|
|
43
|
+
Supporting systemd user services with `PrivateUsers=true` therefore trusts the
|
|
44
|
+
host directory hierarchy against unmapped host peers who own an ancestor and
|
|
45
|
+
can rename it. Sticky world-writable ancestors remain admitted because host
|
|
46
|
+
`/tmp` and `PrivateTmp` appear unmapped under `PrivateUsers`; refusing them would
|
|
47
|
+
disable the default system-temp layout even with a private per-user leaf root.
|
|
48
|
+
An unmapped host owner of such a sticky ancestor could rename its children,
|
|
49
|
+
but a normal host's `/tmp` is root-owned by construction. Mapped foreign owners
|
|
50
|
+
still reject, and this exception never applies to the supplied root or newly
|
|
51
|
+
created workspace. Both maps and mapped-owner exclusions are checked afresh
|
|
52
|
+
whenever admission relies on unmapped ownership, including identity replay;
|
|
53
|
+
unavailable namespace evidence leaves admission unchanged.
|
|
54
|
+
The first admitted unmapped ancestor emits `FS_SAFE_UNMAPPED_TEMP_ANCESTOR`
|
|
55
|
+
through Node's warning event. The warning contains no caller paths.
|
|
56
|
+
|
|
37
57
|
For an already existing canonical root, discovery retains only its immutable
|
|
38
58
|
exact identity. Cleanup-parent retention is provisional: after any native
|
|
39
59
|
capability probe, creation captures and validates the complete ancestry,
|