@openclaw/fs-safe 0.13.1 → 0.15.0
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 +64 -1
- package/README.md +10 -4
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +113 -46
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +4 -4
- package/dist/archive-zip-directory.d.ts +4 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +2 -0
- package/dist/archive-zip-entry.d.ts +6 -2
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +23 -8
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +3 -4
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +107 -31
- package/dist/archive-zip-names.d.ts +1 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +6 -0
- package/dist/archive.js +6 -5
- package/dist/bounded-read-stream.d.ts +0 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +0 -6
- package/dist/copy-publication.d.ts +6 -0
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +3 -0
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +44 -24
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +29 -11
- package/dist/directory-mode-owner.js +5 -5
- package/dist/file-handle-transfer.d.ts +2 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +57 -2
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +18 -4
- package/dist/file-lock-sync-admission.d.ts +19 -0
- package/dist/file-lock-sync-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-admission.js +93 -0
- package/dist/file-lock-sync-root-acquire.d.ts +4 -0
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
- package/dist/file-lock-sync-root-acquire.js +370 -0
- package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
- package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-arbitration.js +66 -0
- package/dist/file-lock-sync-root-held.d.ts +34 -0
- package/dist/file-lock-sync-root-held.d.ts.map +1 -0
- package/dist/file-lock-sync-root-held.js +393 -0
- package/dist/file-lock-sync-root-io.d.ts +44 -0
- package/dist/file-lock-sync-root-io.d.ts.map +1 -0
- package/dist/file-lock-sync-root-io.js +209 -0
- package/dist/file-lock-sync-root-mutation.d.ts +17 -0
- package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
- package/dist/file-lock-sync-root-mutation.js +277 -0
- package/dist/file-lock-sync-root-options.d.ts +20 -0
- package/dist/file-lock-sync-root-options.d.ts.map +1 -0
- package/dist/file-lock-sync-root-options.js +58 -0
- package/dist/file-lock-sync-root-registration.d.ts +2 -0
- package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-registration.js +90 -0
- package/dist/file-lock-sync-root.d.ts +36 -0
- package/dist/file-lock-sync-root.d.ts.map +1 -0
- package/dist/file-lock-sync-root.js +361 -0
- package/dist/file-lock-sync-stale-admission.d.ts +24 -0
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-stale-admission.js +205 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +245 -205
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +5 -1
- package/dist/file-store-sync-directory.d.ts.map +1 -1
- package/dist/file-store-sync-directory.js +14 -7
- package/dist/file-store-sync-write.js +3 -3
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +47 -12
- package/dist/guest-dispatch-python.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +22 -15
- package/dist/json-durable-queue-ownership.d.ts +0 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +0 -6
- package/dist/native-binding.d.ts +8 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-parent-admission.d.ts +3 -2
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +24 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +7 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +202 -172
- package/dist/native-policy-directory-observation.d.ts +12 -0
- package/dist/native-policy-directory-observation.d.ts.map +1 -0
- package/dist/native-policy-directory-observation.js +61 -0
- package/dist/native-policy-parent-windows.d.ts +14 -0
- package/dist/native-policy-parent-windows.d.ts.map +1 -0
- package/dist/native-policy-parent-windows.js +200 -0
- package/dist/native-rename-outcome.d.ts +4 -0
- package/dist/native-rename-outcome.d.ts.map +1 -0
- package/dist/native-rename-outcome.js +8 -0
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +42 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -8
- package/dist/path-prefix.d.ts.map +1 -1
- package/dist/path-prefix.js +30 -8
- package/dist/path-suffix-aliases.d.ts +2 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -1
- package/dist/path-suffix-aliases.js +25 -17
- package/dist/permission-exec.d.ts +2 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +150 -21
- package/dist/permissions-windows.js +1 -1
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +23 -29
- package/dist/pinned-mutation-observation.d.ts +7 -2
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +81 -18
- package/dist/pinned-mutation-shared-route.d.ts +1 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
- package/dist/pinned-mutation-shared-route.js +1 -1
- package/dist/pinned-write-types.d.ts +2 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +75 -121
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +1 -15
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +256 -18
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +62 -70
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -12
- package/dist/replace-file-temp-owner.d.ts +5 -2
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +65 -30
- package/dist/replace-file.js +6 -6
- package/dist/retained-directory-replacement.d.ts +26 -0
- package/dist/retained-directory-replacement.d.ts.map +1 -0
- package/dist/retained-directory-replacement.js +193 -0
- package/dist/root-boundary.d.ts +1 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +4 -0
- package/dist/root-context.d.ts +0 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +0 -3
- package/dist/root-create-input.d.ts +6 -0
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +5 -1
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +5 -4
- package/dist/root-directory-list.d.ts +1 -0
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +20 -11
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +2 -2
- package/dist/root-path-errors.d.ts +1 -0
- package/dist/root-path-errors.d.ts.map +1 -1
- package/dist/root-path-errors.js +11 -2
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +12 -37
- package/dist/root-path.js +1 -13
- package/dist/root-remove.d.ts +1 -0
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +4 -0
- package/dist/root-walk.d.ts +1 -1
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +17 -2
- package/dist/root-write-admission.d.ts +0 -2
- package/dist/root-write-admission.d.ts.map +1 -1
- package/dist/root-write-admission.js +1 -15
- package/dist/root-write-compatibility.d.ts +1 -2
- package/dist/root-write-compatibility.d.ts.map +1 -1
- package/dist/root-write-compatibility.js +17 -68
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +10 -24
- package/dist/root-write-lock-binding.d.ts +15 -0
- package/dist/root-write-lock-binding.d.ts.map +1 -0
- package/dist/root-write-lock-binding.js +162 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +29 -42
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -24
- package/dist/secret-read-policy.d.ts +6 -2
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +26 -2
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +23 -12
- package/dist/sidecar-lock-acquire.d.ts +2 -28
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +288 -199
- package/dist/sidecar-lock-admission-context.d.ts +19 -0
- package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-context.js +60 -0
- package/dist/sidecar-lock-admission-parser.d.ts +43 -0
- package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-parser.js +113 -0
- package/dist/sidecar-lock-admission.d.ts +35 -0
- package/dist/sidecar-lock-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-admission.js +7 -0
- package/dist/sidecar-lock-reclaim.d.ts +9 -4
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +80 -25
- package/dist/sidecar-lock-stale-admission.d.ts +39 -0
- package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-stale-admission.js +232 -0
- package/dist/sidecar-lock-target.d.ts +8 -0
- package/dist/sidecar-lock-target.d.ts.map +1 -0
- package/dist/sidecar-lock-target.js +55 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +100 -16
- package/dist/staged-directory.d.ts +5 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +37 -1
- package/dist/temp-workspace-admission.d.ts.map +1 -1
- package/dist/temp-workspace-admission.js +28 -4
- package/dist/temp-workspace-descriptor.d.ts.map +1 -1
- package/dist/temp-workspace-descriptor.js +9 -27
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +8 -8
- package/dist/walk.d.ts +5 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +19 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -2
- package/docs/advanced.md +1 -1
- package/docs/archive.md +36 -9
- package/docs/atomic.md +85 -8
- package/docs/copy.md +35 -0
- package/docs/file-store.md +19 -0
- package/docs/guest.md +1 -1
- package/docs/json-store.md +5 -0
- package/docs/native-helper.md +10 -3
- package/docs/native.md +8 -0
- package/docs/output.md +6 -0
- package/docs/path-prefix.md +10 -0
- package/docs/path-suffix-aliases.md +51 -6
- package/docs/permissions.md +13 -0
- package/docs/public-api.md +3 -2
- package/docs/root.md +22 -3
- package/docs/sidecar-lock.md +109 -4
- package/docs/staged-file.md +7 -3
- package/docs/temp.md +20 -3
- package/docs/walk.md +67 -1
- package/docs/writing.md +11 -1
- package/package.json +10 -10
- package/dist/darwin-acl.d.ts +0 -4
- package/dist/darwin-acl.d.ts.map +0 -1
- package/dist/darwin-acl.js +0 -24
package/docs/guest.md
CHANGED
|
@@ -80,7 +80,7 @@ disables. Do not omit required fields.
|
|
|
80
80
|
| Rename | `rename srcRoot srcParent srcBasename dstRoot dstParent dstBasename mkdir` | Rename with cross-device copy/delete fallback. |
|
|
81
81
|
| Remove | `remove root parent basename recursive force` | Removes the leaf, or recursively removes its tree. |
|
|
82
82
|
| Make directories | `mkdirp root directory` | Creates missing relative directory components. |
|
|
83
|
-
| List directory | `readdir root directory` | JSON array of `{ name, isDirectory }`; no sorting guarantee. |
|
|
83
|
+
| List directory | `readdir root directory` | JSON array of `{ name, isDirectory, isFile }`; both kind fields are false for symlinks and special entries; no sorting guarantee. |
|
|
84
84
|
|
|
85
85
|
The complete program rejects empty, `.`, `..`, slash-containing, and NUL
|
|
86
86
|
basenames before opening roots or creating parents. Both leaf operands of copy
|
package/docs/json-store.md
CHANGED
|
@@ -85,6 +85,11 @@ For `fileStore(...).json(rel, options)`, `options.durable` overrides the parent
|
|
|
85
85
|
file store's durability, while omission or `undefined` inherits it. Modes,
|
|
86
86
|
identity checks, mutation serialization, and sidecar locking are unchanged.
|
|
87
87
|
|
|
88
|
+
Each `write`, `update`, or `updateOr` invocation captures the retained options'
|
|
89
|
+
`durable` and `trailingNewline` values before queueing, locking, reading, or
|
|
90
|
+
calling the updater. Changes to those options affect later invocations only,
|
|
91
|
+
including when a mutation is waiting behind another operation.
|
|
92
|
+
|
|
88
93
|
The store does **not** validate the parsed value against `T` at runtime — the cast is unchecked. Wrap with a schema (zod/valibot) if the file might be hand-edited or written by another process you don't control.
|
|
89
94
|
|
|
90
95
|
## `read()`
|
package/docs/native-helper.md
CHANGED
|
@@ -95,9 +95,16 @@ clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor
|
|
|
95
95
|
layer owns policy, retries, filters, budgets, modes, cleanup, error
|
|
96
96
|
normalization, and the decision to fall back.
|
|
97
97
|
|
|
98
|
-
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
99
|
-
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
100
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer
|
|
98
|
+
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Direct-child no-replace renames borrow already-retained parent descriptors; deeper relative paths retain the guarded reopen. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
99
|
+
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Direct-child no-replace renames borrow already-retained parents without another `F_GETPATH`; deeper paths retain the guarded reopen. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
100
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, and uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer. The dedicated retained-directory `renameNoReplaceWithIdentity` primitive compares its required exact source receipt with the handle opened for rename before mutation. Its internal mismatch status is normalized to public `path-mismatch`; the legacy four-argument `renameNoReplace` export and its other callers are unchanged. Owned trees are deleted through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table. Descriptor-producing operations also require that same host's synchronous libuv close and request-management APIs before exporting an owned descriptor.
|
|
101
|
+
|
|
102
|
+
`replaceDirectoryAtomic()` requires `renameNoReplaceWithIdentity` before it
|
|
103
|
+
creates a missing target parent. On POSIX the dedicated entry point keeps the
|
|
104
|
+
existing pre-dispatch exact receipt fence but dispatches direct-child names
|
|
105
|
+
through the retained parents without another receipt, duplicate, reopen, or
|
|
106
|
+
macOS `F_GETPATH`; the documented final source-name substitution window remains
|
|
107
|
+
there. Deeper names retain guarded parent traversal.
|
|
101
108
|
|
|
102
109
|
Native primitives back create-only and replacing pinned writes, no-clobber
|
|
103
110
|
`Root.move()`, async sidecar creation, guarded publication, archive acceleration,
|
package/docs/native.md
CHANGED
|
@@ -206,6 +206,14 @@ that fallback does not apply to POSIX no-read modes.
|
|
|
206
206
|
|
|
207
207
|
## JavaScript fallback guarantees and delta
|
|
208
208
|
|
|
209
|
+
Policy-bound parent creation can refresh exact directory facts through the
|
|
210
|
+
retained POSIX descriptor. The optional native observer compares descriptor and
|
|
211
|
+
no-follow pathname metadata and verifies the descriptor's canonical path before
|
|
212
|
+
and after the observation. Ordinary paths can share that observation within one
|
|
213
|
+
synchronous admission phase; callbacks, mutations, and later phases require fresh
|
|
214
|
+
evidence. Unsupported helpers retain the guarded pathname checks. Policy and
|
|
215
|
+
denied-path decisions remain in TypeScript.
|
|
216
|
+
|
|
209
217
|
Public policy does not change with the selected mechanism: traversal and link
|
|
210
218
|
rejection, archive filters/limits/modes, exclusive target creation, source and
|
|
211
219
|
target identity fencing, publication cleanup receipts, and secret/lock policy
|
package/docs/output.md
CHANGED
|
@@ -49,6 +49,12 @@ The requested `path` must name a file. Missing destination parents are created
|
|
|
49
49
|
by the helper because the operation is "produce this output file under the
|
|
50
50
|
root"; callers should choose the filename before calling this API.
|
|
51
51
|
|
|
52
|
+
The helper reads each option once before its first asynchronous operation.
|
|
53
|
+
Changing the options object after invocation does not change the selected
|
|
54
|
+
writer, staging mode, isolation, filename fallback, byte limit, or final mode
|
|
55
|
+
for that write. Workspace writers retain the original options object as their
|
|
56
|
+
callback receiver; sibling writers retain the internal staging receiver.
|
|
57
|
+
|
|
52
58
|
`maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap and `Infinity` disables it. Invalid values reject before the producer or filesystem staging runs.
|
|
53
59
|
|
|
54
60
|
Use `maxBytes` when the external producer can create arbitrarily large files,
|
package/docs/path-prefix.md
CHANGED
|
@@ -56,6 +56,16 @@ they are collapsed. Native realpath alone does not establish this permission
|
|
|
56
56
|
on every platform. Empty components from repeated or trailing separators do
|
|
57
57
|
not introduce a `.` lookup.
|
|
58
58
|
|
|
59
|
+
Raw component queues of at most 32 entries retain the legacy small-array
|
|
60
|
+
consumption path. After both the initial parse and every symlink expansion, a
|
|
61
|
+
longer queue uses forward cursor bookkeeping instead of moving the unprocessed
|
|
62
|
+
suffix for every component. The fixed small-queue bound limits repeated front
|
|
63
|
+
removal while retaining legacy shift-based consumption for shallow paths. This
|
|
64
|
+
asymptotic bound is not a platform performance result; performance acceptance
|
|
65
|
+
requires separate benchmark evidence. Callers should still apply their own
|
|
66
|
+
input-size limits: the helper is synchronous, retains the raw suffix, and
|
|
67
|
+
performs filesystem work for each non-empty existing component.
|
|
68
|
+
|
|
59
69
|
This is a read-only path observation. It neither pins files nor creates a root
|
|
60
70
|
boundary, authorizes access, or guarantees a consistent snapshot during
|
|
61
71
|
concurrent changes. Results can become stale immediately. Use a guarded Root
|
|
@@ -36,6 +36,7 @@ type ProbePathSuffixAliasesOptions = {
|
|
|
36
36
|
directory: string;
|
|
37
37
|
left: string;
|
|
38
38
|
right: string;
|
|
39
|
+
maxDepth?: number;
|
|
39
40
|
shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -44,9 +45,11 @@ function probePathSuffixAliasesSync(
|
|
|
44
45
|
): boolean | undefined;
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
The helper reads
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
The helper reads `directory`, rejects non-string values and supplied paths longer
|
|
49
|
+
than 32,768 code units, then rejects NUL-containing inputs. It resolves the path
|
|
50
|
+
to an absolute path and applies the same limit to that result before reading
|
|
51
|
+
`maxDepth`. Each option is read once. Later option getters or the predicate cannot retarget a
|
|
52
|
+
relative directory by changing the working directory. When
|
|
50
53
|
filesystem observations are needed, the directory is canonicalized and its
|
|
51
54
|
identity is checked; an initial directory alias can be followed.
|
|
52
55
|
|
|
@@ -56,26 +59,68 @@ inputs are rejected with `TypeError`. Windows additionally rejects drive-relativ
|
|
|
56
59
|
components, colons, and reserved device names, including device aliases with
|
|
57
60
|
extensions or trailing ignored characters. On POSIX, backslashes and colons are
|
|
58
61
|
ordinary filename characters. The optional predicate must be a function.
|
|
62
|
+
`maxDepth` defaults to `32` and must be a non-negative safe integer; invalid
|
|
63
|
+
values, including `Infinity`, throw `RangeError` before mutation or an
|
|
64
|
+
identical-suffix return. A value of `0` admits no ordinary nonempty suffix.
|
|
59
65
|
|
|
60
66
|
Both suffixes and the predicate are validated before the identical-suffix fast
|
|
61
67
|
path. Identical, valid, within-budget suffixes return `true` without filesystem
|
|
62
68
|
access or a predicate call. This does not prove that the directory exists or that
|
|
63
69
|
the suffix can be created.
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
The forward-observation allowance never exceeds 32,768, even with a large
|
|
72
|
+
`maxDepth`. A deeper or repeatedly colliding probe can return `undefined` when
|
|
73
|
+
that ceiling is reached. Reverse cleanup still runs outside this allowance.
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
## Resource budgets
|
|
76
|
+
|
|
77
|
+
The default limits for one call are:
|
|
68
78
|
|
|
69
79
|
| Resource | Limit | On exceeding the limit |
|
|
70
80
|
|---|---|---|
|
|
71
81
|
| Each supplied suffix | 8,192 UTF-16 code units | `RangeError` before mutation |
|
|
72
82
|
| Supplied and resolved directory paths | 32,768 UTF-16 code units each | `RangeError` before mutation |
|
|
73
|
-
| Each suffix's component count | 32 | `RangeError` before mutation |
|
|
83
|
+
| Each suffix's component count | `maxDepth`, default 32 | `RangeError` before mutation |
|
|
74
84
|
| Directory-creation attempts | 128 | `undefined` after cleanup |
|
|
75
85
|
| Successfully created probe directories | 64 | `undefined` after cleanup |
|
|
76
86
|
| Forward filesystem observations | 4,096 | `undefined` after cleanup |
|
|
77
87
|
| Each generated actual path | 32,768 UTF-16 code units | `undefined` after cleanup |
|
|
78
88
|
|
|
89
|
+
### Deeper observations
|
|
90
|
+
|
|
91
|
+
Applications comparing deeper prospective paths can explicitly raise `maxDepth`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const prefix = "future/".repeat(32);
|
|
95
|
+
const aliases = probePathSuffixAliasesSync({
|
|
96
|
+
directory: "/trusted/existing-directory",
|
|
97
|
+
left: `${prefix}Report.sqlite`,
|
|
98
|
+
right: `${prefix}report.sqlite`,
|
|
99
|
+
maxDepth: 33,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The 8,192-code-unit suffix limit and 32,768-code-unit path limits remain fixed.
|
|
104
|
+
Ordinary-component validation, Windows path controls, identity checks, and
|
|
105
|
+
cleanup rules also remain unchanged.
|
|
106
|
+
|
|
107
|
+
Operation budgets grow proportionally from the actual admitted suffix depth,
|
|
108
|
+
not the requested `maxDepth`. For actual depth `D`, let `B = max(32, D)`.
|
|
109
|
+
Directory-creation attempts are limited to `4 × B`, successfully created
|
|
110
|
+
directories to `2 × B`, and forward filesystem observations to `min(32,768, 4 × B²)`.
|
|
111
|
+
The quadratic observation allowance accommodates rechecks of owned ancestors.
|
|
112
|
+
|
|
113
|
+
For example, 33 components allow 132 creation attempts, 66 created directories,
|
|
114
|
+
and 4,356 forward observations; 65 components allow 260, 130, and 16,900.
|
|
115
|
+
Specifying a large `maxDepth` for a short suffix keeps the original budgets.
|
|
116
|
+
The suffix-length limit bounds actual depth to at most 4,096, so every budget
|
|
117
|
+
remains a finite safe integer.
|
|
118
|
+
|
|
119
|
+
Admission does not guarantee a boolean result. Repeated collisions, many
|
|
120
|
+
normalization probes, filesystem limits, or identity failures can exhaust the
|
|
121
|
+
budget or prevent an observation. The helper then returns `undefined` after
|
|
122
|
+
cleanup attempts; callers must preserve their explicit ambiguity policy.
|
|
123
|
+
|
|
79
124
|
Input limits are checked even for identical suffixes. Dynamic budgets count work
|
|
80
125
|
across the whole call, including collision retries; removing a probe does not
|
|
81
126
|
restore its creation budget. Cleanup is still attempted when a forward budget is
|
package/docs/permissions.md
CHANGED
|
@@ -102,6 +102,19 @@ characters, including a trailing `…` when truncated. Diagnostics do not copy
|
|
|
102
102
|
stdout or read target file contents. The separate `errorCause` retains the
|
|
103
103
|
original exception for restricted local diagnosis; do not serialize or expose
|
|
104
104
|
it as display text.
|
|
105
|
+
Custom executors may reject with any JavaScript value. The fallback display
|
|
106
|
+
formatter handles primitives directly and reads only string-valued `name` and
|
|
107
|
+
`message` data descriptors through a small, fixed prototype budget. It does not
|
|
108
|
+
coerce objects, invoke accessors, or inspect proxy targets; unavailable display
|
|
109
|
+
facts use a bounded generic reason. Command fields follow the same best-effort
|
|
110
|
+
data-descriptor rule. Raw string, `Buffer`, or genuine `Uint8Array` stderr
|
|
111
|
+
retains the sanitization above. Byte stderr is copied through captured
|
|
112
|
+
typed-array intrinsics into a private bounded snapshot before replacement-based
|
|
113
|
+
UTF-8 decoding; receiver properties, iterators, constructors, and altered
|
|
114
|
+
prototypes are not consulted. Detached or out-of-bounds byte views contribute
|
|
115
|
+
no stderr detail. These diagnostic limits do not relax permission policy:
|
|
116
|
+
incomplete owner or ACL inspection remains unverified, and `errorCause` remains
|
|
117
|
+
the exact rejected value even when no display metadata is safe to obtain.
|
|
105
118
|
The parser and remediation command builders remain on the advanced surface for
|
|
106
119
|
CLIs processing captured `icacls` output or presenting an explicit repair.
|
|
107
120
|
Runtime inspection does not parse that display text. A null DACL reports
|
package/docs/public-api.md
CHANGED
|
@@ -43,8 +43,9 @@ The advanced root-file primitive exports `OpenRootFileParams`,
|
|
|
43
43
|
`RootFileOpenFailureReason`. These are composition types for callers building
|
|
44
44
|
their own pinned-open flow, not substitutes for the higher-level `Root` verbs.
|
|
45
45
|
|
|
46
|
-
`copyFileHandle` and `
|
|
47
|
-
regular files without taking over their
|
|
46
|
+
`copyFileHandle` and `copyFileDescriptorSync` share `CopyFileHandleOptions` to
|
|
47
|
+
transfer bytes between already-open regular files without taking over their
|
|
48
|
+
cursors, lifetime, or publication.
|
|
48
49
|
See [borrowed-handle transfers](copy.md#borrowed-filehandle-transfers).
|
|
49
50
|
|
|
50
51
|
`readDirectoryIdentity`, `assertDirectoryIdentitySync`, and `DirectoryIdentity`
|
package/docs/root.md
CHANGED
|
@@ -72,12 +72,22 @@ The default `order: "sorted"` enumerates and sorts each directory's names;
|
|
|
72
72
|
wide directories. Budget exhaustion yields a `"truncated"` marker by
|
|
73
73
|
default or throws `FsSafeError("too-large")` with `limitBehavior: "throw"`.
|
|
74
74
|
Use `entryFilter(entry)` to return `"include"`, `"skip"`, or
|
|
75
|
-
`"skip-subtree"
|
|
76
|
-
directory; `"skip-subtree"` omits a directory
|
|
75
|
+
`"skip-subtree"`, directly or through a Promise. `"skip"` omits the current
|
|
76
|
+
entry but still descends into a directory; `"skip-subtree"` omits a directory
|
|
77
|
+
and all of its descendants.
|
|
78
|
+
Filters run serially outside metadata batches, with the options object as their
|
|
79
|
+
`this` receiver. After an awaited filter resolves, the walk checks cancellation
|
|
80
|
+
and revalidates the current listing directory and Root identities before using
|
|
81
|
+
the decision. Captured entry metadata retains its snapshot semantics.
|
|
82
|
+
|
|
83
|
+
Cancellation and iterator disposal wait for a pending filter to settle; they do
|
|
84
|
+
not race the callback or close its directory while it is running. Callback
|
|
85
|
+
throws and promise rejections reject the walk through normal cleanup.
|
|
77
86
|
Directory reads remain fail-fast by default. With
|
|
78
87
|
`onDirectoryError: "skip-and-report"`, the iterator instead yields
|
|
79
88
|
`{ relativePath, kind: "directory-error", size: 0, error }` and continues with
|
|
80
|
-
the remaining tree.
|
|
89
|
+
the remaining tree. That policy also covers identity-check failures after an
|
|
90
|
+
awaited filter, while callback failures always reject.
|
|
81
91
|
See [Directory walking](walk.md) for the pure-Node guarantees and the contrast
|
|
82
92
|
with the standalone best-effort walkers.
|
|
83
93
|
|
|
@@ -290,6 +300,15 @@ the caller, which must check authority before its own later writes.
|
|
|
290
300
|
|
|
291
301
|
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. 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.
|
|
292
302
|
|
|
303
|
+
For writes, creates, streams, and copies, parent creation admits the prospective
|
|
304
|
+
file and each missing directory before creating that directory, including on the
|
|
305
|
+
Windows native route. An exact deny on an existing parent does not prevent using
|
|
306
|
+
that parent to write an allowed child. If a deeper missing parent is denied,
|
|
307
|
+
earlier admitted directories may remain; the denied directory and file are not
|
|
308
|
+
created. With `mkdir: false`, missing parents are never created. Native Windows
|
|
309
|
+
policy-aware creation requires the direct-child helper and fails with
|
|
310
|
+
`helper-unavailable` if it is absent.
|
|
311
|
+
|
|
293
312
|
All mutation methods also accept `mutationSymlinks`. `"reject"` rejects symlink
|
|
294
313
|
components; `"follow-parents-within-root"` resolves contained parent directory
|
|
295
314
|
aliases but rejects the final component if it is a symlink, including a dangling
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -9,6 +9,13 @@ normal meaning, and ordinary colon-bearing POSIX paths remain valid.
|
|
|
9
9
|
|
|
10
10
|
JavaScript raw-sidecar creation passes mode `0o600` to that exclusive open. On POSIX, the process umask may further restrict the new file but cannot add group or other access. No pathname `chmod` fallback is used.
|
|
11
11
|
|
|
12
|
+
Root-backed lock records and reclaim guards also claim their final name
|
|
13
|
+
exclusively, including with the native backend. The native path retains the
|
|
14
|
+
admitted parent descriptor and removes incomplete claims only while their exact
|
|
15
|
+
identity remains owned. Ordinary Root creates still stage privately; lock records
|
|
16
|
+
use this internal exclusive-create path so racing contenders can retry without
|
|
17
|
+
an ambiguous rename outcome.
|
|
18
|
+
|
|
12
19
|
```ts
|
|
13
20
|
import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
|
|
14
21
|
|
|
@@ -28,13 +35,32 @@ try {
|
|
|
28
35
|
|
|
29
36
|
The lock file sits next to the protected resource. If a process crashes mid-lock, the next acquirer notices the held entry, inspects its payload (PID, host, acquired-at timestamp), and decides — via `shouldReclaim` (defaulting to "is the lock older than `staleMs`?") — whether it should keep waiting or fail.
|
|
30
37
|
|
|
31
|
-
On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their retained Root capability and ownership receipt. The synchronous `process.on("exit")` handler provides last-chance cleanup for raw locks and reclaim guards. Changed sidecars and failed Root cleanup remain in place; cleanup does not keep retrying during shutdown unless another acquisition re-arms it. Locks acquired with `retainOnExit: true` are exempt from both handlers: their sidecar stays in place after exit and is governed only by the caller's own stale policy. Because exit handlers are globally deduplicated across package copies, `retainOnExit` fails closed with `helper-unavailable` if an older copy that cannot honor it registered the handlers first.
|
|
38
|
+
On natural event-loop shutdown, a globally deduplicated `process.on("beforeExit")` handler attempts asynchronous cleanup of held Root-backed locks through their retained Root capability and ownership receipt. The synchronous `process.on("exit")` handler provides last-chance cleanup for raw locks and raw-path reclaim guards. Changed sidecars and failed Root cleanup remain in place; cleanup does not keep retrying during shutdown unless another acquisition re-arms it. Locks acquired with `retainOnExit: true` are exempt from both handlers: their sidecar stays in place after exit and is governed only by the caller's own stale policy. Because exit handlers are globally deduplicated across package copies, `retainOnExit` fails closed with `helper-unavailable` if an older copy that cannot honor it registered the handlers first.
|
|
39
|
+
|
|
40
|
+
Asynchronous Root-backed stale recovery uses a regular file at the
|
|
41
|
+
`.reclaim` name, created, verified, and removed through that Root. Its ownership
|
|
42
|
+
token and exact bytes are checked after awaited decisions and before stale
|
|
43
|
+
removal; Root mutation policies also apply to the guard. Raw and synchronous
|
|
44
|
+
Root reclaimers use directories and recognize these files as occupied guards.
|
|
45
|
+
Each asynchronous attempt owns its guard directly, outside process-exit cleanup,
|
|
46
|
+
so `beforeExit` cannot release an exclusion still needed by an unsettled attempt.
|
|
47
|
+
Normal completion removes it through the Root. Interrupted creation, revoked
|
|
48
|
+
cleanup authority, identity changes, or process exit can leave the guard in
|
|
49
|
+
place; recover it only after an application-owned liveness check proves the
|
|
50
|
+
attempt has ended. There is no raw-path cleanup fallback. These token/byte
|
|
51
|
+
checks retain the sidecar protocol's cooperative, non-atomic removal boundary.
|
|
52
|
+
The final guard check follows the last sidecar snapshot and parser call, before
|
|
53
|
+
removal. Parsers can run before guard ownership is established or verified;
|
|
54
|
+
invocation is not mutation authority. A failing final parser keeps its error
|
|
55
|
+
even if guard ownership has also changed.
|
|
56
|
+
`manager.reset()` invalidates admission bookkeeping but preserves a pending
|
|
57
|
+
Root guard; let its original attempt settle before retrying that guarded path.
|
|
32
58
|
|
|
33
59
|
Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()`, uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) do not reliably run asynchronous Root cleanup and may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
|
|
34
60
|
|
|
35
|
-
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. 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.
|
|
61
|
+
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.
|
|
36
62
|
|
|
37
|
-
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.
|
|
63
|
+
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.
|
|
38
64
|
|
|
39
65
|
The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
|
|
40
66
|
|
|
@@ -62,6 +88,41 @@ function withFileLockSync<T, TPayload>(targetPath: string, options: FileLockSync
|
|
|
62
88
|
|
|
63
89
|
`managerKey` is an optional identifier used to keep state isolated across multiple lock domains in the same process. Use distinct keys for distinct domains (`"snapshot"`, `"compact"`, `"build"`). If omitted, fs-safe derives one from the target path.
|
|
64
90
|
|
|
91
|
+
Within one manager domain, the canonical target path is the in-process
|
|
92
|
+
arbitration key even when callers supply different explicit `lockPath` values.
|
|
93
|
+
Admission remains pending while payload serialization and stale-policy callbacks
|
|
94
|
+
run, and becomes reentrant only after a matching owner is fully published.
|
|
95
|
+
Foreign owners wait or reach the configured timeout without opening their
|
|
96
|
+
alternate sidecar. Each unguarded attempt still invokes and serializes `payload`
|
|
97
|
+
before a completed foreign holder consumes retry budget, preserving callback and
|
|
98
|
+
retry compatibility; the holder is rechecked before delayed option accessors or
|
|
99
|
+
sidecar I/O. When the candidate resolves to the holder's actual sidecar (the
|
|
100
|
+
default path or an explicitly identical path), asynchronous retries also preserve
|
|
101
|
+
the existing parser observation: bytes are observed first, then its accessor is
|
|
102
|
+
read and the current holder bytes are parsed once per unguarded attempt.
|
|
103
|
+
Distinct alternate sidecars do not trigger that observation. Async acquisition releases
|
|
104
|
+
pending admission before retry backoff;
|
|
105
|
+
synchronous callback reentry cannot let the active stack progress, so it fails
|
|
106
|
+
closed with the normal `file_lock_timeout` fields. Async payload, serialization,
|
|
107
|
+
delayed-option, and stale-policy callbacks carry a process-shared ancestry scope:
|
|
108
|
+
a nested acquisition of the same canonical target in the same manager domain
|
|
109
|
+
fails before waiting on its ancestor, while independent tasks, different targets,
|
|
110
|
+
and different manager domains retain their normal retry behavior. The synchronous
|
|
111
|
+
API uses one process-wide domain. An ancestry snapshot keeps each ancestor that
|
|
112
|
+
is active when the child acquisition starts, even if the current callback's own
|
|
113
|
+
scope already became inactive; later deactivation cannot reclassify that child.
|
|
114
|
+
Detached work started only after every matching ancestor has finished is not
|
|
115
|
+
retained as a descendant. Promise-like callback results are assimilated inside
|
|
116
|
+
that ancestry scope, and the resolved payload crosses the internal return
|
|
117
|
+
boundary in a non-thenable envelope. The helper therefore does not observe a
|
|
118
|
+
stateful payload `then` accessor again outside the reservation.
|
|
119
|
+
|
|
120
|
+
The pending-admission registry coordinates package copies that implement this
|
|
121
|
+
protocol without placing incomplete state in the legacy held-lock map. An older
|
|
122
|
+
already-loaded executable copy does not consult that registry, so a mixed-version
|
|
123
|
+
process cannot rely on the new in-process arbitration until every copy is updated
|
|
124
|
+
and the process is restarted.
|
|
125
|
+
|
|
65
126
|
## Acquire options
|
|
66
127
|
|
|
67
128
|
```ts
|
|
@@ -105,7 +166,11 @@ type FileLockRetryOptions = {
|
|
|
105
166
|
};
|
|
106
167
|
```
|
|
107
168
|
|
|
108
|
-
`payload` is a function so you can re-evaluate it on each retry (e.g. timestamp,
|
|
169
|
+
`payload` is a function so you can re-evaluate it on each retry (e.g. timestamp,
|
|
170
|
+
PID). Callback-valued option accessors are otherwise captured once for an
|
|
171
|
+
acquisition; the asynchronous same-sidecar parser observation above reads
|
|
172
|
+
`parsePayload` once per unguarded attempt. Callback invocation keeps its
|
|
173
|
+
established receiver behavior.
|
|
109
174
|
|
|
110
175
|
Asynchronous acquisition snapshots `targetPath`, an explicit `lockPath`, and
|
|
111
176
|
`lockRoot` before its first asynchronous operation. Cwd-dependent path spellings
|
|
@@ -295,6 +360,46 @@ deadline budget. Errors from descriptor reads/stats or parsing are not treated
|
|
|
295
360
|
as missing snapshots, even when their code is `ENOENT`. Held verification,
|
|
296
361
|
release, and reclaim do not retry open denials.
|
|
297
362
|
|
|
363
|
+
Synchronous `lockRoot` is an authority boundary, not only a containment hint.
|
|
364
|
+
It requires a genuine `Root` returned by the same loaded package copy; a
|
|
365
|
+
structural/custom Root lookalike or a handle constructed by another installed
|
|
366
|
+
copy fails with `helper-unavailable` before any remaining acquisition option or
|
|
367
|
+
nested retry getter, payload evaluation, or filesystem effects. After reading
|
|
368
|
+
`lockRoot` once, the genuine Root and its policies are snapshotted before those
|
|
369
|
+
getters run. Construct `lockRoot` through the same import instance that provides
|
|
370
|
+
the synchronous lock function. The acquirer retains the original Root context,
|
|
371
|
+
exact root, parent, and file identities, and the Root's entry-time read, hardlink,
|
|
372
|
+
mutation-symlink, `denyMutations`, and `assertBeforeMutation` policies. Those
|
|
373
|
+
receipts remain authoritative through same-owner reuse, compromise checks,
|
|
374
|
+
reclaim, explicit release, and process-exit cleanup. If the Root, an admitted
|
|
375
|
+
parent, or the owned entry changes, cleanup leaves the ambiguous path in place.
|
|
376
|
+
Root-backed synchronous records and their exit handler use a separate versioned
|
|
377
|
+
global domain; legacy raw-lock handlers and legacy package copies cannot adopt or
|
|
378
|
+
pathname-delete those records. Root and raw acquisitions never share a
|
|
379
|
+
reentrant reference, even when their owner strings match.
|
|
380
|
+
|
|
381
|
+
As with asynchronous Root-backed acquisition, synchronous target normalization
|
|
382
|
+
does not create the target's parent. An explicit in-root `lockPath` can therefore
|
|
383
|
+
guard an external or not-yet-created target key without creating anything next
|
|
384
|
+
to that target. Missing directories for the sidecar itself are created one
|
|
385
|
+
component at a time through the retained Root policy; the returned `lockPath`
|
|
386
|
+
uses the admitted canonical spelling. This strengthens earlier synchronous
|
|
387
|
+
behavior that treated `lockRoot` as a one-time lexical/canonical bound and used
|
|
388
|
+
raw pathname operations afterward.
|
|
389
|
+
|
|
390
|
+
Windows Root-backed target keys use native existing-ancestor canonicalization,
|
|
391
|
+
so long and short spellings of the same target parent share an arbitration key.
|
|
392
|
+
Sidecar admission applies both the retained mutation policy and read/final-link
|
|
393
|
+
policy before payload evaluation; a dangling final sidecar link is rejected
|
|
394
|
+
without creating its target.
|
|
395
|
+
|
|
396
|
+
Exact Root, parent, and file receipts narrow replacement races but do not make a
|
|
397
|
+
pathname check and the following `open`, `mkdir`, `unlink`, or `rmdir` one atomic
|
|
398
|
+
filesystem operation. A hostile peer with direct write access can still race the
|
|
399
|
+
final syscall. An observed mismatch fails closed and ambiguous entries remain;
|
|
400
|
+
use OS-enforced directory permissions or a native descriptor-relative primitive
|
|
401
|
+
when that attacker model must be excluded.
|
|
402
|
+
|
|
298
403
|
Both synchronous helpers consume the [process-wide lock defaults](config.md#configurefssafelocks-config).
|
|
299
404
|
A synchronous retry sleep is clamped to the remaining finite deadline, so a long or jittered backoff cannot extend the configured timeout or block forever.
|
|
300
405
|
Per-call options take precedence, including zero values; a per-call `retry`
|
package/docs/staged-file.md
CHANGED
|
@@ -105,9 +105,13 @@ touching descriptors.
|
|
|
105
105
|
basename. Empty, dot, dotdot, separators, absolute paths, NUL, control characters,
|
|
106
106
|
drive-relative spellings, and the stage's own name are rejected.
|
|
107
107
|
|
|
108
|
-
With `overwrite: false`, publication is genuine kernel no-replace rename
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
With `overwrite: false`, publication is genuine kernel no-replace rename.
|
|
109
|
+
A native collision raises `FsSafeError("already-exists")`, but ordinary errno
|
|
110
|
+
does not prove that a remote rename never committed. Native rename failures
|
|
111
|
+
without explicit pre-dispatch provenance therefore report `indeterminate`:
|
|
112
|
+
cleanup preserves names and closes descriptors, and further publication rejects.
|
|
113
|
+
Only rejection before rename dispatch leaves the stage eligible for cleanup or
|
|
114
|
+
publication under another name. With
|
|
111
115
|
`overwrite: true`, publication is plain atomic replacement. Neither route
|
|
112
116
|
copies. Both source and destination resolve through the retained original
|
|
113
117
|
parent, with checks immediately before rename and after publication.
|
package/docs/temp.md
CHANGED
|
@@ -221,9 +221,15 @@ A missing workspace returns `"missing"`. A replacement observed at the public
|
|
|
221
221
|
name before quarantine returns `"identity-mismatch"` when the parent is stable;
|
|
222
222
|
an ambiguous parent returns `"indeterminate"`. After successful removal,
|
|
223
223
|
repeated cleanup returns `"missing"` without touching a recreated public name.
|
|
224
|
-
Other statuses remain stable.
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
Other statuses remain stable. Compatible recursive-removal failures propagate
|
|
225
|
+
the exact thrown value, including `undefined`, `null`, `false`, positive or
|
|
226
|
+
negative numeric zero, bigint zero, an empty string, and `NaN`; they are never
|
|
227
|
+
inferred from value identity or truthiness. Uncertain quarantine and
|
|
228
|
+
retained-parent checks instead return
|
|
229
|
+
`"indeterminate"`. After a propagated removal failure, later cleanup returns
|
|
230
|
+
`"indeterminate"` without retrying. Disposal and scoped helpers ignore returned
|
|
231
|
+
statuses, while manual cleanup exposes the result. A terminal descriptor-close
|
|
232
|
+
failure retains its existing precedence if it also fails during settlement.
|
|
227
233
|
|
|
228
234
|
When cleanup is part of a retention or audit decision, inspect the receipt
|
|
229
235
|
instead of treating cleanup as fire-and-forget:
|
|
@@ -410,6 +416,12 @@ files, hardlinks, and changes between the pre-open pathname, opened descriptor,
|
|
|
410
416
|
and current pathname are rejected. The callback must finish and close its
|
|
411
417
|
writer before returning. Its return value is preserved as `result`.
|
|
412
418
|
|
|
419
|
+
Each option is read once before directory creation starts, including both
|
|
420
|
+
callbacks, the temp prefix, isolation, directory and file modes, and sync flags.
|
|
421
|
+
Later changes to the options object do not change the in-flight operation.
|
|
422
|
+
`writeTemp` and `resolveFinalPath` retain their shared internal staging object
|
|
423
|
+
as the callback receiver.
|
|
424
|
+
|
|
413
425
|
Generated temp filenames suffix Windows reserved-device basenames on every
|
|
414
426
|
platform. Before either an ordinary or isolated producer runs, the completed staging
|
|
415
427
|
name must be a nonempty, non-dot path component with no POSIX or Windows
|
|
@@ -536,6 +548,11 @@ await writeViaSiblingTempPath({
|
|
|
536
548
|
If `replaceFileAtomic` does what you need, prefer that. Use
|
|
537
549
|
`writeViaSiblingTempPath` when the producer needs a concrete temp pathname but
|
|
538
550
|
the final destination still needs root-boundary checks.
|
|
551
|
+
|
|
552
|
+
The root, target, callback, fallback filename, and temp prefix are read once
|
|
553
|
+
before setup. Later changes to the parameters do not affect the in-flight
|
|
554
|
+
operation; `writeTemp` retains the original parameters object as its receiver.
|
|
555
|
+
|
|
539
556
|
Its private workspace uses `tempFile()`'s compatible identity-aware cleanup.
|
|
540
557
|
It preserves replacements observed before removal, but retains the final
|
|
541
558
|
pathname-recursive-removal gap described above; this helper does not expose
|
package/docs/walk.md
CHANGED
|
@@ -59,12 +59,43 @@ type WalkDirectoryOptions = {
|
|
|
59
59
|
include?: (entry: WalkDirectoryEntry) => boolean;
|
|
60
60
|
descend?: (entry: WalkDirectoryEntry) => boolean;
|
|
61
61
|
};
|
|
62
|
+
|
|
63
|
+
type AsyncWalkDirectoryOptions = Omit<WalkDirectoryOptions, "include" | "descend"> & {
|
|
64
|
+
include?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
65
|
+
descend?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
66
|
+
};
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
`symlinks` defaults to `"skip"`. `"include"` returns symlink entries without following them. `"follow"` resolves symlinks with `stat()` and may descend into linked directories, so use it only when that is intentional. Already-visited real directories are skipped so symlink cycles do not recurse forever.
|
|
65
70
|
|
|
71
|
+
Before descending into a child directory, `skip` and `include` recheck whether
|
|
72
|
+
that entry has become a symlink, including changes made while a filter waits.
|
|
73
|
+
The explicitly supplied walk root may still be a symlink. This best-effort
|
|
74
|
+
child check does not turn the standalone walker into a confinement boundary;
|
|
75
|
+
use `Root.walk()` when root confinement is required.
|
|
76
|
+
|
|
66
77
|
`include` controls which entries are returned. `descend` controls which directory entries are traversed. A skipped directory can still be returned if `include` accepts it.
|
|
67
78
|
|
|
79
|
+
The asynchronous `walkDirectory()` accepts `AsyncWalkDirectoryOptions`. It resolves each `include` decision before calling `descend`, and resolves descent before reading the directory's children. Decisions run serially in the existing filesystem-order depth-first traversal. Both callbacks retain the supplied options object as their `this` receiver.
|
|
80
|
+
|
|
81
|
+
Absent callbacks and primitive results keep the synchronous selection path. Object and function results are awaited directly, including promises and thenables. For JavaScript callers, nullish results retain the default `true`; other resolved values use their existing truthiness. Return booleans or promises of booleans for the typed API.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import fs from "node:fs/promises";
|
|
85
|
+
import path from "node:path";
|
|
86
|
+
|
|
87
|
+
const scan = await walkDirectory("/safe/workspace", {
|
|
88
|
+
include: (entry) => entry.kind === "file",
|
|
89
|
+
descend: async (entry) => {
|
|
90
|
+
const marked = await fs.access(path.join(entry.path, "SKILL.md"))
|
|
91
|
+
.then(() => true, () => false);
|
|
92
|
+
return !marked;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This prunes a directory after finding its marker without listing that directory's children. Callback throws and promise rejections reject the walk; they are not directory failures in `failedDirs`. Filtering still consumes the examined-entry budget. `WalkDirectoryOptions` and `walkDirectorySync()` remain synchronous; the async options do not add confinement or cancellation to the standalone walker.
|
|
98
|
+
|
|
68
99
|
Unreadable directories are skipped rather than throwing, but every skipped directory is recorded in `failedDirs`. This keeps the helper suitable for best-effort inventories while letting pruning jobs tell an incomplete scan from an empty one: a destructive reconcile that deletes state for paths missing from `entries` must first confirm `failedDirs` holds no real read failures, or a transient `EIO`/`EACCES` blip would be mistaken for mass deletion. Use a stricter root-bounded operation when every entry must be accounted for.
|
|
69
100
|
|
|
70
101
|
## Root-bounded async iteration
|
|
@@ -123,7 +154,9 @@ If a thrown walk failure and directory close both fail, disposal throws a
|
|
|
123
154
|
`SuppressedError` with the close failure in `error` and the original failure in
|
|
124
155
|
`suppressed`, preserving both causes.
|
|
125
156
|
|
|
126
|
-
`entryFilter` is evaluated for each resolved file, directory, or other entry
|
|
157
|
+
`entryFilter` is evaluated for each resolved file, directory, or other entry.
|
|
158
|
+
The `RootWalkEntryFilter` callback returns a `RootWalkEntryFilterResult`
|
|
159
|
+
or a `Promise<RootWalkEntryFilterResult>`:
|
|
127
160
|
|
|
128
161
|
```ts
|
|
129
162
|
for await (const entry of capability.walk("", {
|
|
@@ -147,10 +180,43 @@ The result values are `"include"`, `"skip"`, and `"skip-subtree"`. Plain
|
|
|
147
180
|
`"skip-subtree"` omits that directory and prunes its descendants. Returning
|
|
148
181
|
`"skip-subtree"` for a non-directory is equivalent to `"skip"`.
|
|
149
182
|
|
|
183
|
+
An asynchronous filter can inspect a marker before deciding whether to prune:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
for await (const entry of capability.walk("", {
|
|
187
|
+
symlinkPolicy: "skip",
|
|
188
|
+
entryFilter: async (entry) => {
|
|
189
|
+
if (
|
|
190
|
+
entry.kind === "directory" &&
|
|
191
|
+
await capability.exists(`${entry.relativePath}/SKILL.md`)
|
|
192
|
+
) {
|
|
193
|
+
return "skip-subtree";
|
|
194
|
+
}
|
|
195
|
+
return "include";
|
|
196
|
+
},
|
|
197
|
+
})) {
|
|
198
|
+
consume(entry);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Filters run serially outside metadata batches and retain the supplied options
|
|
203
|
+
object as their `this` receiver. When an awaited filter resolves, the walk
|
|
204
|
+
checks cancellation and revalidates the current listing directory and Root
|
|
205
|
+
identities before using the decision. These checks do not refresh the entry's
|
|
206
|
+
captured metadata or pin a later operation.
|
|
207
|
+
|
|
208
|
+
Cancellation and iterator disposal wait for a pending filter to settle. The
|
|
209
|
+
walk does not race the callback against the abort signal or close its directory
|
|
210
|
+
while the callback is running; callbacks must settle their own work for
|
|
211
|
+
cancellation to finish. Callback throws and promise rejections reject the walk
|
|
212
|
+
through its normal cleanup path, even with `onDirectoryError: "skip-and-report"`.
|
|
213
|
+
|
|
150
214
|
`onDirectoryError` defaults to `"throw"`, preserving the original fail-fast
|
|
151
215
|
contract. `"skip-and-report"` yields a discriminated
|
|
152
216
|
`{ kind: "directory-error", relativePath, size: 0, error }` marker for a
|
|
153
217
|
directory that cannot be resolved or listed, then continues with its siblings.
|
|
218
|
+
This policy also applies when the directory or Root identity recheck after an
|
|
219
|
+
awaited filter fails; callback failures themselves are not directory errors.
|
|
154
220
|
Every examined directory entry consumes `maxEntries` before filtering, so
|
|
155
221
|
`"skip"` cannot turn the iterator into an unbounded traversal. Reporting and
|
|
156
222
|
`"truncated"` markers describe already-reached state and do not authorize
|
package/docs/writing.md
CHANGED
|
@@ -501,7 +501,11 @@ for (const file of files) await fs.write(`${stagingDir}/${file.name}`, file.body
|
|
|
501
501
|
await fs.move(stagingDir, "snapshots/2026-05-05", { overwrite: true });
|
|
502
502
|
```
|
|
503
503
|
|
|
504
|
-
For
|
|
504
|
+
For guarded whole-directory publication, use
|
|
505
|
+
[`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic). Replacing an
|
|
506
|
+
existing target is a two-rename protocol with a temporary target-absence
|
|
507
|
+
interval and conditional no-replace rollback, not a transactional
|
|
508
|
+
commit-or-rollback.
|
|
505
509
|
|
|
506
510
|
### Rotate logs
|
|
507
511
|
|
|
@@ -537,6 +541,12 @@ Windows `Root.write()` and `Root.writeJson()` honor this policy both as a Root d
|
|
|
537
541
|
|
|
538
542
|
The Windows buffered compatibility path resolves permitted in-root aliases before choosing its lock and binds publication to that effective destination. With the existing lock protocol, effective path components beneath the Root must contain only lower-case ASCII letters, digits, `.`, `_`, or `-`, with no trailing `.`. Unsupported spellings, including missing upper-case or non-ASCII names, fail with `path-alias` before mutation; no filesystem case-sensitivity or Unicode-folding behavior is guessed. This restriction does not apply to strict writes. Opaque Windows pathname identities still use strict verification against the retained original descriptor: they never, by themselves, authorize content-based acceptance of a replacement.
|
|
539
543
|
|
|
544
|
+
The library's own lock-destination check permits the writer to reuse parent
|
|
545
|
+
admission within that operation. The lock key and destination checks remain
|
|
546
|
+
unchanged, and evidence is revoked when the locked operation finishes. Supplying
|
|
547
|
+
an `assertBeforeMutation` callback retains full parent admission because caller
|
|
548
|
+
code can change the filesystem before dispatch.
|
|
549
|
+
|
|
540
550
|
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID. Recover only under external authority that excludes every competing writer; see [File lock](sidecar-lock.md#stale-recovery-guarded-remove-if-unchanged).
|
|
541
551
|
|
|
542
552
|
## See also
|