@openclaw/openshell-sandbox 2026.10.5 → 2026.10.7
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/dist/index.js +16 -18
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +48 -0
- package/node_modules/@openclaw/fs-safe/README.md +14 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-link.js +169 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
- package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/file-hash.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +57 -18
- package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +19 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +18 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native.d.ts +5 -3
- package/node_modules/@openclaw/fs-safe/dist/native.js +25 -6
- package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +36 -47
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +5 -3
- package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +12 -11
- package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
- package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +12 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +6 -92
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +11 -5
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +35 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +22 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +13 -5
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +15 -5
- package/node_modules/@openclaw/fs-safe/docs/native.md +51 -2
- package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
- package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
- package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
- package/node_modules/@openclaw/fs-safe/docs/reading.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
- package/node_modules/@openclaw/fs-safe/docs/store.md +11 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +45 -80
- package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
- package/node_modules/@openclaw/fs-safe/package.json +18 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
|
@@ -5,9 +5,9 @@ import { watchStreamPaths } from "./watch-stream.js";
|
|
|
5
5
|
export function watchBinding(mode) {
|
|
6
6
|
if (mode === "poll")
|
|
7
7
|
return;
|
|
8
|
-
const binding = getNativeBinding(
|
|
8
|
+
const binding = getNativeBinding("watchRegister", "watchAdd", "watchUnregister", ...(process.platform === "darwin" ? ["watchConfigure", "watchEntries"] : []));
|
|
9
9
|
// Bun TSFN teardown remains unqualified; guarded native scans remain usable.
|
|
10
|
-
if (binding
|
|
10
|
+
if (binding && !process.versions.bun && !process.versions.deno && ["linux", "darwin", "win32"].includes(process.platform))
|
|
11
11
|
return binding;
|
|
12
12
|
if (mode === "events" || getFsSafeNativeConfig().mode === "require") {
|
|
13
13
|
throw new FsSafeError("helper-unavailable", "native watch events are unavailable", { details: { operation: "watch" } });
|
|
@@ -41,18 +41,19 @@ public static partial class FsSafeWindowsBridge {
|
|
|
41
41
|
|
|
42
42
|
sealed class Failure : Exception {
|
|
43
43
|
public readonly string Code;
|
|
44
|
-
public
|
|
44
|
+
public readonly uint? Errno;
|
|
45
|
+
public Failure(string code, string message, uint? errno=null) : base(message) { Code=code; Errno=errno; }
|
|
45
46
|
}
|
|
46
47
|
static Failure OsFailure(uint error, string operation, bool identity=false, bool fullIdentity=false) {
|
|
47
48
|
// Native security calls retain EACCES; shared handle identity calls use EPERM.
|
|
48
49
|
string code=error==5 ? (identity ? "EPERM" : "EACCES") : error==80 || error==183 ? "EEXIST" : error==2 || error==3 ? "ENOENT" :
|
|
49
50
|
identity && (error==112 || error==39) ? "ENOSPC" : identity && (error==32 || error==33) ? "EBUSY" :
|
|
50
51
|
fullIdentity && (error==1 || error==50 || error==87) ? "ENOTSUP" : "EIO";
|
|
51
|
-
return new Failure(code, operation+" failed with Windows error "+error);
|
|
52
|
+
return new Failure(code, operation+" failed with Windows error "+error, error);
|
|
52
53
|
}
|
|
53
54
|
static Failure CreateFailure(uint error) {
|
|
54
55
|
var failure=OsFailure(error,"create private directory",true);
|
|
55
|
-
return failure.Code=="EPERM" ? new Failure("EACCES",failure.Message) : failure;
|
|
56
|
+
return failure.Code=="EPERM" ? new Failure("EACCES",failure.Message,failure.Errno) : failure;
|
|
56
57
|
}
|
|
57
58
|
static void Require(bool condition, string code, string message) {
|
|
58
59
|
if (!condition) throw new Failure(code,message);
|
|
@@ -305,7 +306,8 @@ public static partial class FsSafeWindowsBridge {
|
|
|
305
306
|
var failure=primary as Failure;
|
|
306
307
|
throw new Failure(failure==null ? "EIO" : failure.Code,
|
|
307
308
|
(failure==null ? "private directory validation failed" : failure.Message)+
|
|
308
|
-
"; owned-handle cleanup failed with Windows error "+Marshal.GetLastWin32Error()
|
|
309
|
+
"; owned-handle cleanup failed with Windows error "+Marshal.GetLastWin32Error(),
|
|
310
|
+
failure==null ? null : failure.Errno);
|
|
309
311
|
}
|
|
310
312
|
throw;
|
|
311
313
|
}
|
|
@@ -330,7 +332,11 @@ public static partial class FsSafeWindowsBridge {
|
|
|
330
332
|
} else if(operation=="path") result=InspectPath(path);
|
|
331
333
|
else throw new Failure("EINVAL","unknown Windows security operation");
|
|
332
334
|
return Row("ok",true,"result",result);
|
|
333
|
-
} catch(Failure error) {
|
|
335
|
+
} catch(Failure error) {
|
|
336
|
+
var reply=Row("ok",false,"code",error.Code,"message",error.Message);
|
|
337
|
+
if(error.Errno.HasValue) reply.Add("errno",error.Errno.Value);
|
|
338
|
+
return reply;
|
|
339
|
+
}
|
|
334
340
|
catch(Exception) { return Row("ok",false,"code","EIO","message","Windows security descriptor processing failed"); }
|
|
335
341
|
}
|
|
336
342
|
}
|
|
@@ -85,7 +85,15 @@ function parseReplyValue(stdout, operation) {
|
|
|
85
85
|
if (typeof response.code !== "string" || !codes.has(response.code) || typeof response.message !== "string") {
|
|
86
86
|
unverified("Windows security command returned an invalid failure");
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
if (response.errno !== undefined &&
|
|
89
|
+
(typeof response.errno !== "number" || !Number.isInteger(response.errno) || response.errno < 0 || response.errno > 0xffff_ffff)) {
|
|
90
|
+
unverified("Windows security command returned an invalid native error number");
|
|
91
|
+
}
|
|
92
|
+
const failure = Object.assign(new Error(response.message), { code: response.code });
|
|
93
|
+
if (response.errno !== undefined) {
|
|
94
|
+
Object.defineProperty(failure, "errno", { value: response.errno, enumerable: true, configurable: true, writable: true });
|
|
95
|
+
}
|
|
96
|
+
throw failure;
|
|
89
97
|
}
|
|
90
98
|
if (operation === "create" && (!record(response.result) || response.result.created !== true || !isFullIdentity(response.result.identity))) {
|
|
91
99
|
unverified("Windows security command did not verify private-directory creation");
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Test-only handle that omits delete sharing. */
|
|
2
|
+
export declare function holdWindowsSharingLock(filePath: string): Disposable & {
|
|
3
|
+
close(): void;
|
|
4
|
+
};
|
|
5
|
+
/** Test-only attribute updates preserve unspecified bits on the opened object. */
|
|
6
|
+
export declare function setWindowsFileAttributes(filePath: string, attrs: {
|
|
7
|
+
readOnly?: boolean;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
system?: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
/** Test-only physical mapping reader; lcn -1 represents sparse clusters. */
|
|
12
|
+
export declare function readWindowsFileExtents(filePath: string): {
|
|
13
|
+
vcn: bigint;
|
|
14
|
+
lcn: bigint;
|
|
15
|
+
clusters: bigint;
|
|
16
|
+
}[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { requireNativeBinding } from "./native.js";
|
|
4
|
+
function call(operation) {
|
|
5
|
+
if (process.platform !== "win32") {
|
|
6
|
+
throw new FsSafeError("unsupported-platform", "Windows test fixtures require Windows");
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return operation();
|
|
10
|
+
}
|
|
11
|
+
catch (cause) {
|
|
12
|
+
if (cause instanceof FsSafeError)
|
|
13
|
+
throw cause;
|
|
14
|
+
throw new FsSafeError("helper-failed", "Windows test fixture operation failed", { cause });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Test-only handle that omits delete sharing. */
|
|
18
|
+
export function holdWindowsSharingLock(filePath) {
|
|
19
|
+
return call(() => {
|
|
20
|
+
const native = requireNativeBinding();
|
|
21
|
+
if (!native.holdWindowsSharingLock)
|
|
22
|
+
throw new FsSafeError("helper-unavailable", "Windows sharing fixture unavailable");
|
|
23
|
+
const owner = native.holdWindowsSharingLock(path.toNamespacedPath(filePath));
|
|
24
|
+
let closed = false;
|
|
25
|
+
const close = () => {
|
|
26
|
+
if (closed)
|
|
27
|
+
return;
|
|
28
|
+
closed = true;
|
|
29
|
+
call(() => owner.close());
|
|
30
|
+
};
|
|
31
|
+
return Object.freeze({ close, [Symbol.dispose]: close });
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Test-only attribute updates preserve unspecified bits on the opened object. */
|
|
35
|
+
export function setWindowsFileAttributes(filePath, attrs) {
|
|
36
|
+
call(() => {
|
|
37
|
+
const native = requireNativeBinding();
|
|
38
|
+
if (!native.setWindowsFileAttributes)
|
|
39
|
+
throw new FsSafeError("helper-unavailable", "Windows attribute fixture unavailable");
|
|
40
|
+
native.setWindowsFileAttributes(path.toNamespacedPath(filePath), attrs);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Test-only physical mapping reader; lcn -1 represents sparse clusters. */
|
|
44
|
+
export function readWindowsFileExtents(filePath) {
|
|
45
|
+
return call(() => {
|
|
46
|
+
const native = requireNativeBinding();
|
|
47
|
+
if (!native.readWindowsFileExtents)
|
|
48
|
+
throw new FsSafeError("helper-unavailable", "Windows extent fixture unavailable");
|
|
49
|
+
return native.readWindowsFileExtents(path.toNamespacedPath(filePath));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -185,6 +185,15 @@ policy to the complete archive, including entries that extraction would strip
|
|
|
185
185
|
or skip. Known UTF-8 and Unicode Path names are checked during physical admission;
|
|
186
186
|
legacy names retain backend-selected decoding and undergo the same collision
|
|
187
187
|
check before callbacks or selected bytes are returned.
|
|
188
|
+
Native legacy decoding is CP437 when neither the UTF-8 flag nor a valid Unicode
|
|
189
|
+
Path override declares UTF-8, even if the raw bytes also happen to form valid
|
|
190
|
+
UTF-8. Invalid flagged UTF-8 and invalid Unicode Path CRC/encoding fail closed;
|
|
191
|
+
the native metadata boundary enforces these checks before decoder normalization.
|
|
192
|
+
Native ZIP inspection also retains rejection of malformed Unicode Comment
|
|
193
|
+
checksums and UTF-8, including in entries unrelated to a requested payload.
|
|
194
|
+
Comment CRCs follow the native decoder's historical decoded-comment semantics;
|
|
195
|
+
valid sequential Unicode overrides retain their decoder order. Public physical
|
|
196
|
+
admission continues to reject duplicate critical Unicode Path fields.
|
|
188
197
|
|
|
189
198
|
ZIP end-record admission searches the bounded comment window for signatures
|
|
190
199
|
while retaining complete comment-length and ambiguity checks. Dense signature
|
|
@@ -244,6 +253,9 @@ remaining components are skipped before the filter callback, but still count
|
|
|
244
253
|
toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
|
|
245
254
|
copies the admitted payload range to this accepted output path, so depth checks,
|
|
246
255
|
collision checks, writes, and mode application agree.
|
|
256
|
+
ZIP directories naming the relative archive root (for example `./`) follow the
|
|
257
|
+
same skip rule on both backends. Portable preflight retains their relative `./`
|
|
258
|
+
spelling; a literal absolute `/` entry is still rejected.
|
|
247
259
|
|
|
248
260
|
An `entryFilter` sees the validated **canonical effective archive path before
|
|
249
261
|
stripping**, entry kind, and declared size. On every JavaScript and native
|
|
@@ -348,6 +360,10 @@ A failure before publication preserves a pre-existing file, and rejection does
|
|
|
348
360
|
not grant authority to delete a substituted file or alias. Failed extraction does not
|
|
349
361
|
restore overwritten contents. Active destination mutations and their guarded
|
|
350
362
|
cleanup still finish before rejection; no later destination mutation begins.
|
|
363
|
+
On supported native POSIX paths, each active destination ancestor uses its retained
|
|
364
|
+
descriptor plus fresh named-identity and physical-path observations at every guard
|
|
365
|
+
boundary. These checks are never cached or reduced to the deepest directory alone.
|
|
366
|
+
Other paths retain the full pathname checks.
|
|
351
367
|
Portable ZIP output is not eligible for publication until its stream closes or
|
|
352
368
|
the defensive `FileHandle` close succeeds. If that fallback close rejects,
|
|
353
369
|
`extractArchive()` propagates the error and publishes no entry from the staged
|
|
@@ -100,6 +100,29 @@ On Linux x64 or arm64 with Docker, run `pnpm build`, copy the matching GNU artif
|
|
|
100
100
|
artifact and run native security and no-replace move tests in Rocky Linux 8
|
|
101
101
|
(glibc 2.28). Both GNU architectures execute this load test on matching runners.
|
|
102
102
|
|
|
103
|
+
### Windows ARM64 builds
|
|
104
|
+
|
|
105
|
+
`windows-11-arm` builds and runs the ARM64 MSVC addon using native ARM64 Node
|
|
106
|
+
and Rust, and separately tests x64 Node and Rust under Windows emulation. Both
|
|
107
|
+
lanes run the native suite, main-thread and Worker private-creation/descriptor
|
|
108
|
+
proof, and root-only npm/pnpm install smoke. PE inspection matches the loaded
|
|
109
|
+
consumer binary's hash, architecture and required NT imports. Omitted-optionals
|
|
110
|
+
creation probes in the x64-emulated cell have a 600-second deadline: three
|
|
111
|
+
complete measurements had a 183.18-second median, with 140 PowerShell launches
|
|
112
|
+
accounting for 98–99% of wall time. This is over three times that median, rounded
|
|
113
|
+
up to a whole minute. Native ARM64 and x64 cells, and every other consumer probe,
|
|
114
|
+
retain their 120-second deadlines. Release builds
|
|
115
|
+
also run the ARM64 native security and Worker tests before uploading the binding.
|
|
116
|
+
|
|
117
|
+
The only linked C codec is `zstd-sys`; `cargo test --workspace --locked` and the
|
|
118
|
+
release addon build compile it for `aarch64-pc-windows-msvc`. Bzip2 uses
|
|
119
|
+
`libbz2-rs-sys`, and deflate uses `miniz_oxide`, both Rust implementations.
|
|
120
|
+
The portable WASM build uses the checksum-pinned x64 WASI SDK under Windows
|
|
121
|
+
emulation because that SDK has no Windows ARM64 host archive. Its compiler and
|
|
122
|
+
archiver produce wasm32 only; the addon still uses MSVC for the selected target.
|
|
123
|
+
Bun 1.4.2 has no Windows ARM64 artifact; the existing Windows x64 Bun lanes remain
|
|
124
|
+
the Bun runtime proof. Native Windows ARM64 support is qualified with Node.
|
|
125
|
+
|
|
103
126
|
## Test
|
|
104
127
|
|
|
105
128
|
```bash
|
|
@@ -262,14 +285,14 @@ OS/CPU/libc selection, a native-required SHA-256 operation, and fresh-process
|
|
|
262
285
|
optionals. Omitted-optionals installs also verify that all public subpaths can
|
|
263
286
|
be imported, without implying every operation remains available.
|
|
264
287
|
|
|
265
|
-
Host-only smoke supplies the
|
|
288
|
+
Host-only smoke supplies the seven foreign packages using their unchanged real
|
|
266
289
|
manifests and clearly marked synthetic, non-executable payloads. Every foreign
|
|
267
290
|
metadata/tarball endpoint is checked before installation, so a missing fixture
|
|
268
291
|
cannot masquerade as successful platform filtering. These temporary fixtures
|
|
269
292
|
never enter `packages/`, release artifacts, or the publish manifest. They prove
|
|
270
293
|
installer filtering, not foreign native compilation or execution. Full release
|
|
271
|
-
collection uses the actual
|
|
272
|
-
`pnpm package:collect` after assembling all
|
|
294
|
+
collection uses the actual eight collected native tarballs instead. Run it with
|
|
295
|
+
`pnpm package:collect` after assembling all eight real bindings; missing targets
|
|
273
296
|
fail collection. `pnpm package:collect --allow-host-only` exercises the same
|
|
274
297
|
lifecycle boundary locally but proves only the host. Both collection commands
|
|
275
298
|
require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standalone
|
|
@@ -277,7 +300,7 @@ require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standa
|
|
|
277
300
|
direct `node` invocation without lifecycle metadata is unsupported. Archive
|
|
278
301
|
codecs and their dependencies are packed from the installed dependency graph.
|
|
279
302
|
|
|
280
|
-
PR CI builds and executes all
|
|
303
|
+
PR CI builds and executes all eight shipped bindings. The existing check names
|
|
281
304
|
stay stable; extra runner/runtime combinations add checks without changing the
|
|
282
305
|
repository ruleset. Native lanes run Node 24; the GNU host lanes also exercise
|
|
283
306
|
Bun 1.4.2 (as do macOS and Windows).
|
|
@@ -294,6 +317,7 @@ Bun 1.4.2 (as do macOS and Windows).
|
|
|
294
317
|
| Native check | `macos-15-intel` | x64 | Node 24, Bun 1.4.2 |
|
|
295
318
|
| Native check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 24, Bun 1.4.2 |
|
|
296
319
|
| Native check | `windows-2022` (standard hosted) | x64 | Node 24, Bun 1.4.2 |
|
|
320
|
+
| Windows ARM64 host | `windows-11-arm` | arm64 native and x64 emulated | Node 24 |
|
|
297
321
|
| Native check (Alpine 3.24) | `ubuntu-latest` | x64 / musl | Node 24 |
|
|
298
322
|
| Native check (Alpine 3.24) | `ubuntu-24.04-arm` | arm64 / musl | Node 24 |
|
|
299
323
|
| GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-latest` | x64 / glibc | Node 24 |
|
|
@@ -302,11 +326,17 @@ Bun 1.4.2 (as do macOS and Windows).
|
|
|
302
326
|
| Coverage | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22 |
|
|
303
327
|
|
|
304
328
|
Both musl lanes also run root-only package smoke with the real host binding.
|
|
305
|
-
The
|
|
329
|
+
The eight-target source build matrix still runs on release tags. The smoke writes manager versions,
|
|
306
330
|
cases, and synthetic-fixture scope to `release-artifacts/consumer-proof.json`.
|
|
307
331
|
|
|
308
332
|
## Docs
|
|
309
333
|
|
|
334
|
+
`pnpm docs:check` runs examples against the built package and compiles every
|
|
335
|
+
TypeScript block in `README.md`, the lock API signatures, and the custom stale-lock
|
|
336
|
+
policy example against its declarations. Run `pnpm build` first.
|
|
337
|
+
The compiler supplies typed application context for excerpted variables such as
|
|
338
|
+
`download`; package API calls and callback values remain fully type-checked.
|
|
339
|
+
|
|
310
340
|
The docs site is rendered recursively from Markdown files under `docs/` by `scripts/build-docs-site.mjs`. Build locally to preview:
|
|
311
341
|
|
|
312
342
|
```bash
|
|
@@ -68,12 +68,24 @@ On Linux, automatic byte copying also uses the native binding when available. It
|
|
|
68
68
|
|
|
69
69
|
Tree-copy cleanup attempts every acquired close once, even when another close fails. Portable file handles close output before input; completed directories close their pinned source before target; the public wrapper then closes its source before the destination parent. A copy, identity, metadata, native-clone, or cancellation failure already observed at one of those scopes remains the reported value instead of being replaced by cleanup. If that scope otherwise succeeded, its first close failure is reported unchanged. Concurrent siblings have no global structural close order: the first observed sibling failure stops new work while admitted work settles. Portable copying records caller cancellation when it occurs, so a later abort during cleanup cannot replace an earlier copy failure.
|
|
70
70
|
|
|
71
|
+
For guarded per-file copying, [`Root.copyIn`](writing.md#copy-metadata-and-source-links) can preserve timestamps and Windows non-permission attributes with `preserveMetadata`, and copy literal links with `sourceSymlinks: "copy-link"`. Permission selection remains separate. Windows reparse payload handling is shared with this tree copier.
|
|
72
|
+
|
|
71
73
|
Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
|
|
72
74
|
|
|
73
75
|
Native Windows byte copies can store large zero-filled chunks as sparse ranges when the destination is initially empty and its filesystem supports sparse files. This still reads every source byte and creates an independent copy.
|
|
74
76
|
|
|
75
77
|
The ReFS backend rejects files with alternate data streams and unsupported reparse-point types instead of silently losing their contents. Symbolic links and junctions are preserved.
|
|
76
78
|
|
|
79
|
+
For successful native ReFS clones through `copyTree` or `Root.copyIn`, all full
|
|
80
|
+
clusters are block-cloned; a final partial cluster may be copied. Both preserve exact bytes and logical file size.
|
|
81
|
+
The shared implementation keeps EOF at the source size and rounds only the final
|
|
82
|
+
clone request to the allocation-cluster boundary. Microsoft's
|
|
83
|
+
[block-cloning rules](https://learn.microsoft.com/en-us/windows/win32/fileio/block-cloning#restrictions-and-remarks)
|
|
84
|
+
require cluster-aligned ranges and setting destination EOF before cloning;
|
|
85
|
+
rounding a request does not promise physical sharing of a partial final cluster.
|
|
86
|
+
Windows x64 and ARM64 tests compare every full-cluster LCN for both APIs,
|
|
87
|
+
including the 4 MiB shared prefix of a 4 MiB + 1 byte file.
|
|
88
|
+
|
|
77
89
|
Failed ReFS clones attempt to remove their partial output through the retained directory handles. On Windows versions that reject the ignore-readonly deletion flag, clone rollback retries without that flag so ordinary output can be removed. It never clears readonly attributes: readonly output can remain, and the original clone error includes the cleanup failure. Other processes retaining output handles can delay deletion beyond settlement; a failed call does not guarantee an absent destination.
|
|
78
90
|
|
|
79
91
|
XFS and ZFS preserve regular-file and directory modes, timestamps, extended attributes, and ACLs. They reject special files, symlink extended attributes, non-UTF-8 names, and directory nesting deeper than 128 levels. Hardlinked source files become independent reflinked files. Portable byte copying preserves file contents, empty directories, modes where supported, file and directory timestamps, and literal symbolic links; it does not promise ownership, ACL, extended-attribute, alternate-stream, or sparse-layout preservation. On Windows, byte copying rejects unresolved symbolic links because Node does not expose their file/directory link type; resolved links keep their literal target and source type. POSIX dangling links are preserved. Choose a copying policy that meets the caller's metadata requirements; automatic copying can select either path.
|
|
@@ -192,6 +204,16 @@ Byte copying retains fractional file and directory access/modification timestamp
|
|
|
192
204
|
|
|
193
205
|
After building the host native binding, run `pnpm test test/clone.test.ts test/copy-tree.test.ts`. APFS tests can use the normal macOS temporary directory. For Btrfs, ReFS, XFS, or ZFS, set `FS_SAFE_CLONE_TEST_ROOT` to an existing writable directory on that filesystem. The test creates and cleans only its own temporary children. An explicitly configured unsupported directory fails the test rather than silently skipping platform proof. XFS and ZFS metadata tests require the `attr` and `acl` utilities.
|
|
194
206
|
|
|
207
|
+
The `ReFS copyIn` CI workflow creates a disposable 2 GiB expanding VHD on
|
|
208
|
+
Windows Server 2025 x64 and Windows 11 ARM64. It formats the latter as a Dev
|
|
209
|
+
Drive, verifies that the mounted filesystem is ReFS, and sets
|
|
210
|
+
`FS_SAFE_CLONE_TEST_ROOT`. `test/root-copy-refs.test.ts` runs the built package on
|
|
211
|
+
the main thread and in a real Node Worker, checks shared physical extents with
|
|
212
|
+
`readWindowsFileExtents`, independent writes, exclusive publication, byte limits,
|
|
213
|
+
cancellation cleanup, and revocation between file copies. Both jobs also run the
|
|
214
|
+
tree clone suite, then detach and delete the VHD even after failure. A failed
|
|
215
|
+
volume setup fails the job; it never substitutes NTFS or skips the ReFS proof.
|
|
216
|
+
|
|
195
217
|
Run `node scripts/clone-xfs-proof.mjs MOUNT` on a real XFS volume to verify the public API, hashes, independent writes, and shared physical extents. It requires `filefrag` from `e2fsprogs`. Add `no-reflink` for an XFS fixture formatted with reflinks disabled; strict copying must fail and automatic copying must succeed through byte copying.
|
|
196
218
|
|
|
197
219
|
Run `node scripts/clone-zfs-proof.mjs MOUNT POOL` on a dedicated, otherwise idle Linux ZFS pool with compression and deduplication disabled. It verifies both `copyTree` and `Root.copyIn` through hashes and changes in the documented `bclonesaved` pool counter. It requires `zfs`, `zpool`, and `findmnt`, including permission to run `zpool sync`. Add `no-reflink` for a pool without block cloning to verify strict refusal and automatic byte fallback. The script creates and removes only its temporary directory; it does not create pools or change their properties.
|
|
@@ -224,9 +224,11 @@ count limits need not mean the entire filesystem lacks hard-link support.
|
|
|
224
224
|
`link-required` and `link-or-copy` do not use `renameat2` and are unchanged.
|
|
225
225
|
|
|
226
226
|
`"link-required"` propagates an unsupported hardlink failure.
|
|
227
|
-
`"link-or-copy"` falls back only for `EPERM`, `EXDEV`, `ENOTSUP`,
|
|
227
|
+
`"link-or-copy"` falls back only for `EACCES`, `EPERM`, `EXDEV`, `ENOTSUP`,
|
|
228
228
|
`EOPNOTSUPP`, or `ENOSYS`; `isHardlinkFallbackError()` exposes that exact
|
|
229
|
-
classifier
|
|
229
|
+
classifier, including Android/Termux hardlink permission refusals. A genuine
|
|
230
|
+
permission denial still fails the fallback's exclusive target creation; `EEXIST`
|
|
231
|
+
never permits fallback. The fallback copies from the pinned source into a `wx` target,
|
|
230
232
|
fsyncs it, and fences source and target identity and content before reporting
|
|
231
233
|
success. `parentReceipt`, when supplied, must name the target's direct parent.
|
|
232
234
|
|
|
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
|
|
|
38
38
|
|
|
39
39
|
`cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
|
|
40
40
|
|
|
41
|
+
Native Windows failures, including private directory and file creation failures,
|
|
42
|
+
retain the original numeric Win32 error in `errno` (for example, 183 for
|
|
43
|
+
`ERROR_ALREADY_EXISTS` or 5 for `ERROR_ACCESS_DENIED`). This also applies to the
|
|
44
|
+
Windows security command fallback. A wrapped failure retains that number on its
|
|
45
|
+
original `cause`; the semantic `FsSafeError.code` remains the contract. Policy
|
|
46
|
+
checks and preflight rejections that do not execute a failing Windows operation
|
|
47
|
+
have no Win32 error number. Node-managed filesystem errors retain Node's own errno.
|
|
48
|
+
|
|
41
49
|
Guarded write preparation describes permission, read-only filesystem, and disk-space
|
|
42
50
|
failures with messages such as `permission denied (EACCES)` or
|
|
43
51
|
`no space left on device (ENOSPC)`. Other errno failures include their code in
|
|
@@ -56,6 +56,8 @@ can replace only an **empty directory created concurrently**; non-empty
|
|
|
56
56
|
directories and non-directory targets are refused as `EEXIST`. An existing
|
|
57
57
|
empty directory observed by the absence check is also refused. No empty claim
|
|
58
58
|
directory is exposed before publishing the completed source.
|
|
59
|
+
An `ENOTDIR` rename failure rechecks the source before reporting a destination
|
|
60
|
+
collision; a detected source substitution reports `ESTALE` instead of `EEXIST`.
|
|
59
61
|
|
|
60
62
|
Identity checks and mutation remain separate, not source-name compare-and-swap.
|
|
61
63
|
The fallback verifies the published identity afterward. A file fallback failure
|
|
@@ -106,6 +106,7 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
|
|
106
106
|
| `@openclaw/fs-safe/json` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants. |
|
|
107
107
|
| `@openclaw/fs-safe/store` | `fileStore()`, `fileStoreSync()`, and `jsonStore<T>()`. |
|
|
108
108
|
| `@openclaw/fs-safe/secret` | Secret file read/write helpers. |
|
|
109
|
+
| `@openclaw/fs-safe/pipe` | Native-owned, reopenable anonymous pipes for one-shot child input. |
|
|
109
110
|
| `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `writeTextAtomic`, `replaceDirectoryAtomic`, `movePathWithCopyFallback`. |
|
|
110
111
|
| `@openclaw/fs-safe/durability` | Pinned directories, strict sync, durable directory creation, exclusive publication, and streaming SHA-256. |
|
|
111
112
|
| `@openclaw/fs-safe/temp` | `tempWorkspace`, `withTempWorkspace`, sync variants, `resolveSecureTempRoot`. |
|
|
@@ -125,9 +126,11 @@ Use the main entry for the common surface, or the focused subpaths when you want
|
|
|
125
126
|
|
|
126
127
|
`@openclaw/fs-safe` bundles an import-free WASM build of its Rust TAR parser and zstd/bzip2 codecs for guarded [archive extraction and bounded entry reads](archive.md). Plain TAR, gzip, zstd, and bzip2 work in `off` and missing-native `auto`, including installs with all optional dependencies omitted; gzip uses Node's built-in decoder. These archive fallbacks need no runtime interpreter, download, or consumer compiler. ZIP fallback uses lazily loaded optional `jszip` and reports a missing-dependency error without it. Public subpaths remain safe to import with all optional dependencies omitted, but imports do not prove native availability. Native `require` remains strict, and an available native operation's failure never triggers a WASM retry.
|
|
127
128
|
|
|
128
|
-
There are no peer dependencies. Exact-version optional packages carry the
|
|
129
|
+
There are no peer dependencies. Exact-version optional packages carry the
|
|
129
130
|
native targets and npm-compatible OS, CPU, and Linux libc filters install only
|
|
130
|
-
the matching binary.
|
|
131
|
+
the matching binary. Windows ARM64 Node selects
|
|
132
|
+
`@openclaw/fs-safe-win32-arm64-msvc`; x64 Node on the same host selects the x64
|
|
133
|
+
package. Consumers do not run a Rust build, download code at
|
|
131
134
|
runtime, or execute a postinstall step. Omitting optional dependencies keeps
|
|
132
135
|
fallback-capable operations working in `auto` or `off`. Native-only
|
|
133
136
|
features, including strict owned-tree temp cleanup, retained-directory staging,
|
|
@@ -145,8 +148,11 @@ before deploying with native mode `require` or native-only features.
|
|
|
145
148
|
The native addon supports kernels without `openat2` (before Linux 5.6) and
|
|
146
149
|
containers returning `ENOSYS` or probe-time `EPERM` for that syscall. Beneath
|
|
147
150
|
opens use a guarded no-follow component walk and report `best-effort`.
|
|
148
|
-
Nested no-clobber moves retain
|
|
149
|
-
|
|
151
|
+
Nested no-clobber moves retain native parent admission and exact identity checks;
|
|
152
|
+
keep native mode enabled. If `renameat2(RENAME_NOREPLACE)` is also unsupported,
|
|
153
|
+
Linux `auto` uses [move fallbacks](writing.md#fs-move-from-to-options), including
|
|
154
|
+
a directory rename that can replace a concurrently created empty directory.
|
|
155
|
+
Native `require` refuses these fallbacks. Strict bounded cleanup still
|
|
150
156
|
requires `openat2` with `RESOLVE_NO_XDEV` and reports `helper-unavailable`
|
|
151
157
|
without it. See [Linux capability behavior and limits](native.md#linux-without-openat2).
|
|
152
158
|
|
|
@@ -180,7 +186,9 @@ available native operation's error never triggers a command retry. See
|
|
|
180
186
|
### Supported native platforms
|
|
181
187
|
|
|
182
188
|
Prebuilt bindings cover Linux x64/arm64 (GNU glibc **2.28 or newer**, or musl),
|
|
183
|
-
macOS x64/arm64,
|
|
189
|
+
macOS x64/arm64, Windows x64/arm64, and FreeBSD 14.4+ x64/arm64. FreeBSD provides the
|
|
190
|
+
[documented subset](native.md#freebsd-14-4-and-newer); its other operations retain
|
|
191
|
+
guarded fallbacks in `auto`. The GNU baseline includes RHEL 8, Rocky Linux 8,
|
|
184
192
|
and AlmaLinux 8. A compatible Node 22+ runtime and the kernel/filesystem features
|
|
185
193
|
required by each operation are still necessary; a loadable addon alone does not
|
|
186
194
|
guarantee every native capability. Systems older than glibc 2.28 are outside the
|
|
@@ -16,11 +16,11 @@ Current releases install the matching exact-version optional platform package.
|
|
|
16
16
|
- Run on Node.js 22 or newer.
|
|
17
17
|
- Update `@openclaw/fs-safe` and regenerate every lock or shrinkwrap file your
|
|
18
18
|
deployment consumes.
|
|
19
|
-
- Keep optional dependencies enabled
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
- Keep optional dependencies enabled for the matching native package and the
|
|
20
|
+
JSZip fallback. Current TAR/gzip/zstd/bzip2 fallbacks are bundled and work
|
|
21
|
+
with optional dependencies omitted. Native loading no longer depends on
|
|
22
|
+
Python. See [runtime dependencies](install.md#runtime-dependencies) for the
|
|
23
|
+
current installation contract.
|
|
24
24
|
|
|
25
25
|
If you call `resolveRootPath()` or `resolveRootPathSync()` directly, upgrade to
|
|
26
26
|
0.5: versions through 0.4.7 could approve an in-root symlink traversal that
|
|
@@ -5,8 +5,10 @@ description: "How fs-safe loads its platform-specific native filesystem primitiv
|
|
|
5
5
|
|
|
6
6
|
# Native helper policy
|
|
7
7
|
|
|
8
|
-
`@openclaw/fs-safe` declares
|
|
9
|
-
x64/arm64 (glibc or musl), macOS x64/arm64, and
|
|
8
|
+
`@openclaw/fs-safe` declares exact-version optional packages for Linux
|
|
9
|
+
x64/arm64 (glibc or musl), macOS x64/arm64, Windows x64/arm64, and FreeBSD x64/arm64.
|
|
10
|
+
The FreeBSD 14.4+ bindings expose the [qualified subset](native.md#freebsd-14-4-and-newer).
|
|
11
|
+
Package-manager
|
|
10
12
|
OS, CPU, and libc filters install only the matching package. The loader requires
|
|
11
13
|
that package lazily, without runtime downloads, postinstall scripts, or a
|
|
12
14
|
consumer Rust build.
|
|
@@ -33,6 +35,12 @@ See [Archive extraction](archive.md) for native, bundled WASM, and ZIP backends.
|
|
|
33
35
|
Native archive-operation failures are terminal; `auto` does not retry them through a
|
|
34
36
|
fallback. `require` rejects missing bindings or required capabilities.
|
|
35
37
|
|
|
38
|
+
A loaded binding may provide only a subset of native operations. Each operation
|
|
39
|
+
checks its required capabilities before native dispatch: missing capabilities
|
|
40
|
+
select the existing guarded fallback in `auto`, or report `helper-unavailable`
|
|
41
|
+
in `require`. A failure from an available native method is still an operation
|
|
42
|
+
failure; it is not treated as a missing capability.
|
|
43
|
+
|
|
36
44
|
Windows owner/DACL inspection, private-directory creation, and secure reads can
|
|
37
45
|
use [PowerShell fallbacks](install.md#windows-security-fallback) in `auto` and
|
|
38
46
|
`off`; `require` stays strict, and native operation failures remain terminal.
|
|
@@ -109,10 +117,12 @@ is authoritative for operation and platform guarantees.
|
|
|
109
117
|
|
|
110
118
|
## Migration from the Python helper
|
|
111
119
|
|
|
112
|
-
The
|
|
113
|
-
|
|
120
|
+
The Python configuration bridge has been removed. Old Python environment
|
|
121
|
+
settings are ignored without a warning, so an old `require` or `off` setting
|
|
122
|
+
does not select the corresponding native policy. Configure
|
|
123
|
+
`FS_SAFE_NATIVE_MODE` or `configureFsSafeNative()` explicitly. Follow the
|
|
114
124
|
[0.5 migration checklist](migrating-to-0.5.md#2-replace-python-helper-configuration)
|
|
115
|
-
for
|
|
125
|
+
for the removed settings and current precedence.
|
|
116
126
|
|
|
117
127
|
## Related pages
|
|
118
128
|
|
|
@@ -5,6 +5,9 @@ description: "The platform-specific native bindings, fd-relative beneath model,
|
|
|
5
5
|
|
|
6
6
|
# Native architecture
|
|
7
7
|
|
|
8
|
+
See [platform filesystem capabilities](platform-capabilities.md) for Linux
|
|
9
|
+
write leases, public Darwin ACL facts, and Windows test fixtures.
|
|
10
|
+
|
|
8
11
|
`@openclaw/fs-safe` uses native bindings that supply mechanisms Node does
|
|
9
12
|
not expose directly. The Rust layer is deliberately not a second policy engine.
|
|
10
13
|
TypeScript owns trusted-root selection, path validation, archive filtering,
|
|
@@ -14,12 +17,46 @@ platform syscall sequence that can preserve the boundary.
|
|
|
14
17
|
|
|
15
18
|
Every operation that has an equivalent safe Node implementation keeps that
|
|
16
19
|
guarded JavaScript path. Native loading is lazy; installs do not compile Rust,
|
|
17
|
-
run postinstall code, or fetch binaries at runtime.
|
|
20
|
+
run postinstall code, or fetch binaries at runtime. Exact-version optional
|
|
18
21
|
packages are filtered by OS, CPU, and Linux libc, so an installation receives
|
|
19
|
-
only its matching prebuilt binding.
|
|
22
|
+
only its matching prebuilt binding. Windows ships both x64 and ARM64 MSVC
|
|
23
|
+
bindings; selection follows the Node process architecture. An x64 Node process
|
|
24
|
+
on Windows ARM64 uses the x64 binding under emulation.
|
|
20
25
|
Native-only operations fail explicitly
|
|
21
26
|
instead of substituting a weaker implementation.
|
|
22
27
|
|
|
28
|
+
## FreeBSD 14.4 and newer
|
|
29
|
+
|
|
30
|
+
The FreeBSD x64 and arm64 packages provide native anonymous pipes, descriptor
|
|
31
|
+
close and POSIX canonicalization. The larger Linux/macOS/Windows filesystem backend is excluded
|
|
32
|
+
from these binaries: its Unix gates contain platform-specific resolver, clone,
|
|
33
|
+
copy, observation and cleanup implementations. FreeBSD does not export stubs for
|
|
34
|
+
those operations. In `auto`, each operation selects its existing guarded JavaScript
|
|
35
|
+
or WASM backend; in `require`, a missing required capability fails with
|
|
36
|
+
`helper-unavailable`. Pure JavaScript APIs retain their ordinary behavior.
|
|
37
|
+
|
|
38
|
+
| Native capability | FreeBSD binding |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `createPipe`, `closeOwnedFd`, `canonicalizePath` | Implemented; real pipe, descriptor and pathname syscalls |
|
|
41
|
+
| Beneath open/create/mkdir/link, replace/no-replace rename and identity-fenced rename | Absent; guarded fallback where supported |
|
|
42
|
+
| Directory observations, retained files/symlinks, entry publication, owned-tree and root removal | Absent; guarded fallback or explicit native-only rejection |
|
|
43
|
+
| File/tree clone, native copy/hash, archive inspection/extraction and buffer readers | Absent; JavaScript/WASM fallback where supported |
|
|
44
|
+
| Native watch | Absent; polling fallback in `auto` |
|
|
45
|
+
| Darwin ACL and Windows security/identity methods | Not applicable |
|
|
46
|
+
|
|
47
|
+
The release workflow builds inside FreeBSD 14.4 VMs and checks installed-package
|
|
48
|
+
behavior on the main thread and a real Worker. ARM64 uses QEMU CPU emulation
|
|
49
|
+
with a real FreeBSD kernel. This is not a Linux cross-build runtime test. Build
|
|
50
|
+
locally on FreeBSD with `pnpm native:build:freebsd`; the JavaScript/WASM distribution
|
|
51
|
+
can be prepared separately with `pnpm build`. There is no qualified FreeBSD Bun
|
|
52
|
+
artifact in the runtime matrix. FreeBSD x64 installed-package proof covers npm
|
|
53
|
+
and pnpm. FreeBSD arm64 covers npm only because pnpm does not publish a native
|
|
54
|
+
binary for that target. CI prepares the ARM64 proof harness dependencies on Linux
|
|
55
|
+
and syncs them into the VM; consumer installations and runtime proof run inside
|
|
56
|
+
FreeBSD, without running pnpm there.
|
|
57
|
+
Reopening pipe descriptors through `/dev/fd` requires the standard `fdescfs`
|
|
58
|
+
mount there; the VM proof mounts it before testing reopen, inheritance and EOF.
|
|
59
|
+
|
|
23
60
|
## The beneath model
|
|
24
61
|
|
|
25
62
|
A trusted directory descriptor is the capability. Native operations accept
|
|
@@ -75,6 +112,11 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
|
75
112
|
this layer only through the host executable's paired libuv descriptor bridge;
|
|
76
113
|
missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
|
|
77
114
|
or add-on CRT descriptor namespace.
|
|
115
|
+
`Root.copyIn` uses the same ReFS integrity, sparse-file, and cluster-aligned
|
|
116
|
+
block-clone operations as tree cloning, through retained source and destination
|
|
117
|
+
parent handles. The exclusive stage enters the existing guarded publication
|
|
118
|
+
path; transfer failures retain its cleanup receipt. It does not inherit the
|
|
119
|
+
tree API's source metadata policy.
|
|
78
120
|
|
|
79
121
|
The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
|
|
80
122
|
or `present` for the opened object's extended ACL. It synchronously borrows the
|
|
@@ -375,6 +417,13 @@ silently selects the JavaScript fallback in `auto`, throws typed
|
|
|
375
417
|
`helper-unavailable` in `require`, and is never inspected in `off`. Tests reject
|
|
376
418
|
`child_process`, `exec`, or `spawn` usage in the loader.
|
|
377
419
|
|
|
420
|
+
## Anonymous pipes
|
|
421
|
+
|
|
422
|
+
The lean [`pipe` subpath](pipe.md) creates native-owned anonymous pipes. Linux and
|
|
423
|
+
FreeBSD use atomic close-on-exec creation; Darwin reports its non-atomic flag
|
|
424
|
+
setup explicitly. Each end must close through its native owner, including when
|
|
425
|
+
a Node stream uses it in a Worker.
|
|
426
|
+
|
|
378
427
|
## Related pages
|
|
379
428
|
|
|
380
429
|
- [Native helper policy](native-helper.md)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Anonymous pipes
|
|
2
|
+
|
|
3
|
+
`@openclaw/fs-safe/pipe` exposes a synchronous, native anonymous pipe for
|
|
4
|
+
one-shot input to a child process. Both ends are reopenable through
|
|
5
|
+
`/proc/self/fd/N` on Linux and `/dev/fd/N` on Darwin and FreeBSD. This supports
|
|
6
|
+
programs that open an inherited descriptor as a pathname.
|
|
7
|
+
|
|
8
|
+
On FreeBSD, reopening through `/dev/fd/N` requires `fdescfs` mounted at `/dev/fd`.
|
|
9
|
+
An administrator can mount it with `mount -t fdescfs fdesc /dev/fd`; pipe creation
|
|
10
|
+
itself does not require that mount.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createPipe, type OwnedPipeSync } from "@openclaw/fs-safe/pipe";
|
|
14
|
+
|
|
15
|
+
const pipe: OwnedPipeSync = createPipe();
|
|
16
|
+
try {
|
|
17
|
+
// Pass pipe.reader.fd as a numeric child_process spawn stdio entry.
|
|
18
|
+
// Keep both owners alive until their respective users finish.
|
|
19
|
+
} finally {
|
|
20
|
+
pipe.reader.close();
|
|
21
|
+
pipe.writer.close();
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The frozen result has `reader` and `writer` members of the existing
|
|
26
|
+
`OwnedFileDescriptorSync` type (`readonly fd: number`, `close(): void`, and
|
|
27
|
+
`[Symbol.dispose](): void`) and `readonly atomicCloseOnExec: boolean`.
|
|
28
|
+
Each end closes idempotently through its native owner. A close attempt consumes
|
|
29
|
+
ownership even if it throws; do not retry with a raw close or use the fd again.
|
|
30
|
+
The pair itself does not have a `close` or disposal method.
|
|
31
|
+
|
|
32
|
+
Linux and FreeBSD create both ends atomically with close-on-exec set.
|
|
33
|
+
Darwin sets the flag on both ends immediately after creation and reports
|
|
34
|
+
`atomicCloseOnExec: false`: a concurrent native fork/exec can inherit an end
|
|
35
|
+
in that short window. No JavaScript async work intervenes, but this is not an
|
|
36
|
+
atomic inheritance guarantee. Partial setup failure closes both ends.
|
|
37
|
+
|
|
38
|
+
Importing the subpath is lazy and does not load the addon or the secret-file
|
|
39
|
+
modules. Calling `createPipe()` requires a compatible native helper; disabled
|
|
40
|
+
or missing helpers throw `FsSafeError` with `helper-unavailable`. Windows and
|
|
41
|
+
other unsupported platforms throw `unsupported-platform`. Native creation
|
|
42
|
+
failures throw `helper-failed` with the original error as `cause`.
|
|
43
|
+
The cause's `code` preserves the native errno, including `EMFILE` (the process
|
|
44
|
+
descriptor limit) and `ENFILE` (the system-wide limit).
|
|
45
|
+
|
|
46
|
+
## Handing the writer to a Node stream
|
|
47
|
+
|
|
48
|
+
A native-created fd must close through its owner, including in Workers.
|
|
49
|
+
Provide the stream's custom close hook; do not let Node close it directly.
|
|
50
|
+
This is the writer handoff contract: retain the owner in the hook rather than
|
|
51
|
+
detaching its descriptor.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createWriteStream, write, writev } from "node:fs";
|
|
55
|
+
import { createPipe } from "@openclaw/fs-safe/pipe";
|
|
56
|
+
|
|
57
|
+
const pipe = createPipe();
|
|
58
|
+
const stream = createWriteStream("", {
|
|
59
|
+
fd: pipe.writer.fd,
|
|
60
|
+
fs: {
|
|
61
|
+
write,
|
|
62
|
+
writev,
|
|
63
|
+
close(_fd, callback) {
|
|
64
|
+
try {
|
|
65
|
+
pipe.writer.close();
|
|
66
|
+
callback(null);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
// After spawning with pipe.reader.fd in the child's stdio:
|
|
74
|
+
pipe.reader.close();
|
|
75
|
+
stream.end("one-shot input");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Once the stream is created, its lifetime owns use of the writer: keep an error
|
|
79
|
+
listener, destroy it on cancellation, and wait for `close` before considering
|
|
80
|
+
the fd released. If stream construction fails, close both owners. Closing the
|
|
81
|
+
last writer delivers EOF; unread bytes remain available to the reader.
|