@openclaw/openshell-sandbox 2026.9.6 → 2026.9.8
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/assets/icon.png +0 -0
- package/dist/index.js +75 -229
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
- package/node_modules/@openclaw/fs-safe/README.md +24 -6
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
- package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
- package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
- package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
- package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
- package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
- package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
- package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
- package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
- package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
- package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +16 -17
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
- package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
- package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
- package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
- package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
- package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
- package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
- package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
- package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
- package/node_modules/@openclaw/fs-safe/package.json +13 -9
- 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-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-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/node_modules/p-limit/index.d.ts +2 -0
- package/node_modules/p-limit/index.js +17 -1
- package/node_modules/p-limit/package.json +1 -1
- package/node_modules/p-limit/readme.md +2 -0
- package/package.json +6 -6
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { NativeWindowsDescriptorSecurityFacts, NativeWindowsSecurityFacts } from "./native-binding.js";
|
|
2
|
+
import type { FsSafeNativeMode } from "./native-config.js";
|
|
3
|
+
import { type DescriptorFacts } from "./windows-security-facts.js";
|
|
4
|
+
export declare const WINDOWS_SECURITY_BATCH_MAX_BYTES: number;
|
|
2
5
|
/** Cleanup must preserve stages still reachable by an unsettled command. */
|
|
3
6
|
export declare function hasUnsettledWindowsSecurityCommand(error: unknown): boolean;
|
|
7
|
+
export declare function readWindowsSecurityFactsBatch(paths: readonly string[], options: {
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
native: boolean;
|
|
10
|
+
mode: FsSafeNativeMode;
|
|
11
|
+
}): Promise<DescriptorFacts[]>;
|
|
4
12
|
export declare function readWindowsSecurityFactsCommand(targetPath: string): NativeWindowsSecurityFacts;
|
|
5
13
|
export declare function inspectWindowsDescriptorCommand(fd: number): Promise<NativeWindowsDescriptorSecurityFacts>;
|
|
6
14
|
export declare function createPrivateWindowsDirectoryCommand(targetPath: string, expectedParentIdentity?: string): Promise<{
|
|
@@ -3,10 +3,11 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { DEFAULT_PERMISSION_EXEC_TIMEOUT_MS, PermissionCommandError } from "./permission-exec.js";
|
|
5
5
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
6
|
-
import { parseWindowsSecurityCommandFacts } from "./windows-security-facts.js";
|
|
6
|
+
import { parseWindowsOwnerAndDaclFacts, parseWindowsSecurityCommandFacts, unverified } from "./windows-security-facts.js";
|
|
7
7
|
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
8
8
|
const TERMINATION_GRACE_MS = 1_000;
|
|
9
9
|
const FULL_IDENTITY = /^[0-9a-f]{16}:[0-9a-f]{32}$/;
|
|
10
|
+
export const WINDOWS_SECURITY_BATCH_MAX_BYTES = 16 * 1024 * 1024;
|
|
10
11
|
function isFullIdentity(identity) {
|
|
11
12
|
return typeof identity === "string" && identity.length === 49 && FULL_IDENTITY.test(identity);
|
|
12
13
|
}
|
|
@@ -50,9 +51,6 @@ function command(operation, params) {
|
|
|
50
51
|
},
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
function unverified(message, cause) {
|
|
54
|
-
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
55
|
-
}
|
|
56
54
|
function record(value) {
|
|
57
55
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
58
56
|
}
|
|
@@ -79,6 +77,10 @@ function parseReplyValue(stdout, operation) {
|
|
|
79
77
|
unverified("Windows security command returned an incomplete response");
|
|
80
78
|
}
|
|
81
79
|
if (!response.ok) {
|
|
80
|
+
if (operation === "paths" &&
|
|
81
|
+
(response.code === "helper-unavailable" || response.code === "too-large") && typeof response.message === "string") {
|
|
82
|
+
throw new FsSafeError(response.code, response.message);
|
|
83
|
+
}
|
|
82
84
|
const codes = new Set(["EACCES", "EPERM", "EEXIST", "ENOENT", "ENOTSUP", "EIO", "EBADF", "ELOOP", "ENOTDIR", "EINVAL", "ENOSPC", "EBUSY"]);
|
|
83
85
|
if (typeof response.code !== "string" || !codes.has(response.code) || typeof response.message !== "string") {
|
|
84
86
|
unverified("Windows security command returned an invalid failure");
|
|
@@ -115,13 +117,13 @@ class WindowsSecurityCommandError extends PermissionCommandError {
|
|
|
115
117
|
timedOut;
|
|
116
118
|
creationOutcome;
|
|
117
119
|
processExitConfirmed;
|
|
118
|
-
constructor(file, durationMs, receipt, timedOut) {
|
|
119
|
-
super(file, durationMs, receipt);
|
|
120
|
+
constructor(file, durationMs, receipt, timedOut, timeoutMs = DEFAULT_PERMISSION_EXEC_TIMEOUT_MS) {
|
|
121
|
+
super(file, durationMs, receipt, timeoutMs);
|
|
120
122
|
this.timedOut = timedOut;
|
|
121
123
|
this.creationOutcome = receipt.creationOutcome;
|
|
122
124
|
this.processExitConfirmed = receipt.processExitConfirmed;
|
|
123
125
|
if (timedOut)
|
|
124
|
-
this.message = `Windows permission inspection timed out after ${
|
|
126
|
+
this.message = `Windows permission inspection timed out after ${timeoutMs}ms`;
|
|
125
127
|
if (!receipt.processExitConfirmed)
|
|
126
128
|
this.message += "; process exit was not confirmed";
|
|
127
129
|
else if (!receipt.outputClosed)
|
|
@@ -159,11 +161,12 @@ export function hasUnsettledWindowsSecurityCommand(error) {
|
|
|
159
161
|
return pending.length > 0;
|
|
160
162
|
}
|
|
161
163
|
function ignoreLateError() { }
|
|
162
|
-
async function execute(operation, params) {
|
|
163
|
-
const
|
|
164
|
+
async function execute(operation, params, request) {
|
|
165
|
+
const selected = request ?? command(operation, params);
|
|
166
|
+
const { file, args, env, input, timeoutMs = DEFAULT_PERMISSION_EXEC_TIMEOUT_MS, maxOutputBytes = MAX_OUTPUT_BYTES } = selected;
|
|
164
167
|
const startedAt = performance.now();
|
|
165
168
|
return await new Promise((resolve, reject) => {
|
|
166
|
-
const child = spawn(file, args, { windowsHide: true, env, stdio: [params.fd ?? "ignore", "pipe", "pipe"] });
|
|
169
|
+
const child = spawn(file, args, { windowsHide: true, env, stdio: [input === undefined ? params.fd ?? "ignore" : "pipe", "pipe", "pipe"] });
|
|
167
170
|
const output = [];
|
|
168
171
|
const errors = [];
|
|
169
172
|
let bytes = 0;
|
|
@@ -183,7 +186,7 @@ async function execute(operation, params) {
|
|
|
183
186
|
const timeout = setTimeout(() => {
|
|
184
187
|
timedOut = true;
|
|
185
188
|
fail(deadlineError());
|
|
186
|
-
},
|
|
189
|
+
}, timeoutMs);
|
|
187
190
|
const finish = (outputClosed) => {
|
|
188
191
|
if (settled)
|
|
189
192
|
return;
|
|
@@ -196,6 +199,18 @@ async function execute(operation, params) {
|
|
|
196
199
|
child.removeListener("error", onChildError);
|
|
197
200
|
child.on("error", ignoreLateError);
|
|
198
201
|
const cleanupErrors = [];
|
|
202
|
+
if (input !== undefined && child.stdin) {
|
|
203
|
+
child.stdin.removeListener("error", fail);
|
|
204
|
+
child.stdin.on("error", ignoreLateError);
|
|
205
|
+
if (!outputClosed) {
|
|
206
|
+
try {
|
|
207
|
+
child.stdin.destroy();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
cleanupErrors.push(error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
199
214
|
for (const [stream, collect] of [[child.stdout, collectOutput], [child.stderr, collectErrors]]) {
|
|
200
215
|
if (!stream)
|
|
201
216
|
continue;
|
|
@@ -224,7 +239,7 @@ async function execute(operation, params) {
|
|
|
224
239
|
cause: failure, pid: child.pid ?? null, code: exitCode, signal: exitSignal, stderr: Buffer.concat(errors),
|
|
225
240
|
processExitConfirmed, outputClosed, terminationSignalSent, terminationError, cleanupErrors,
|
|
226
241
|
...(operation === "create" ? { creationOutcome: "unconfirmed" } : {}),
|
|
227
|
-
}, timedOut));
|
|
242
|
+
}, timedOut, timeoutMs));
|
|
228
243
|
}
|
|
229
244
|
else {
|
|
230
245
|
try {
|
|
@@ -263,7 +278,7 @@ async function execute(operation, params) {
|
|
|
263
278
|
if (failed || settled)
|
|
264
279
|
return;
|
|
265
280
|
bytes += chunk.length;
|
|
266
|
-
if (bytes <=
|
|
281
|
+
if (bytes <= maxOutputBytes)
|
|
267
282
|
chunks.push(chunk);
|
|
268
283
|
else
|
|
269
284
|
fail(new Error("Windows security command exceeded its output budget"));
|
|
@@ -285,7 +300,7 @@ async function execute(operation, params) {
|
|
|
285
300
|
};
|
|
286
301
|
const onClose = (code, signal) => {
|
|
287
302
|
onExit(code, signal);
|
|
288
|
-
if (!failed && performance.now() - startedAt >=
|
|
303
|
+
if (!failed && performance.now() - startedAt >= timeoutMs) {
|
|
289
304
|
timedOut = true;
|
|
290
305
|
failed = true;
|
|
291
306
|
failure = deadlineError();
|
|
@@ -301,11 +316,47 @@ async function execute(operation, params) {
|
|
|
301
316
|
child.stderr?.on("error", fail);
|
|
302
317
|
child.stdout?.on("data", collectOutput);
|
|
303
318
|
child.stderr?.on("data", collectErrors);
|
|
304
|
-
if (!child.stdout || !child.stderr) {
|
|
319
|
+
if (!child.stdout || !child.stderr || (input !== undefined && !child.stdin)) {
|
|
305
320
|
// Node reports spawn errors on nextTick, which can follow the current microtask.
|
|
306
321
|
missingPipes = setImmediate(() => { if (!failed && !settled)
|
|
307
322
|
fail(new Error("Windows security command output pipes are unavailable")); });
|
|
308
323
|
}
|
|
324
|
+
if (input !== undefined && child.stdin) {
|
|
325
|
+
child.stdin.on("error", fail);
|
|
326
|
+
try {
|
|
327
|
+
child.stdin.end(input, "utf8");
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
fail(error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
export async function readWindowsSecurityFactsBatch(paths, options) {
|
|
336
|
+
const invocation = command("paths", {});
|
|
337
|
+
const request = {
|
|
338
|
+
...invocation,
|
|
339
|
+
input: JSON.stringify(paths),
|
|
340
|
+
timeoutMs: options.timeoutMs,
|
|
341
|
+
maxOutputBytes: WINDOWS_SECURITY_BATCH_MAX_BYTES,
|
|
342
|
+
};
|
|
343
|
+
if (options.native) {
|
|
344
|
+
request.file = process.execPath;
|
|
345
|
+
request.args = ["--", fileURLToPath(new URL("./owner-dacl-batch-worker.js", import.meta.url))];
|
|
346
|
+
request.env = {
|
|
347
|
+
...Object.fromEntries(Object.entries(invocation.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH", "FS_SAFE_OWNER_DACL_BATCH_MODE"].includes(key.toUpperCase()))),
|
|
348
|
+
FS_SAFE_OWNER_DACL_BATCH_MODE: options.mode,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const response = await execute("paths", {}, request);
|
|
352
|
+
if (!Array.isArray(response) || response.length !== paths.length) {
|
|
353
|
+
unverified("Windows security command returned an incomplete path batch");
|
|
354
|
+
}
|
|
355
|
+
return response.map((row, index) => {
|
|
356
|
+
if (!record(row) || row.path !== paths[index]) {
|
|
357
|
+
unverified("Windows security command returned a mismatched path batch");
|
|
358
|
+
}
|
|
359
|
+
return parseWindowsOwnerAndDaclFacts(row.security);
|
|
309
360
|
});
|
|
310
361
|
}
|
|
311
362
|
export function readWindowsSecurityFactsCommand(targetPath) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { NativeWindowsSecurityFacts } from "./native-binding.js";
|
|
2
|
+
export type DescriptorFacts = Pick<NativeWindowsSecurityFacts, "ownerSid" | "currentUserSid" | "daclPresent" | "isLocal" | "aceListComplete" | "unsupportedAceTypes" | "aces">;
|
|
3
|
+
export declare function unverified(message: string, cause?: unknown): never;
|
|
4
|
+
/** Validate policy-free observations crossing the isolated batch transport. */
|
|
5
|
+
export declare function parseWindowsOwnerAndDaclFacts(value: unknown): DescriptorFacts;
|
|
2
6
|
/** Raw reporting retains unknown flag bits; secure admission validates them below. */
|
|
3
7
|
export declare function parseWindowsSecurityCommandFacts(value: unknown): NativeWindowsSecurityFacts;
|
|
4
8
|
/** Native and command observations share the same fail-closed admission policy. */
|
|
@@ -6,8 +6,8 @@ const WORLD_SIDS = new Set([
|
|
|
6
6
|
"s-1-1-0", "s-1-5-11", "s-1-5-32-545", "s-1-5-7",
|
|
7
7
|
"s-1-5-32-546", "s-1-5-4", "s-1-5-2",
|
|
8
8
|
]);
|
|
9
|
-
function unverified(message) {
|
|
10
|
-
throw new FsSafeError("permission-unverified", message);
|
|
9
|
+
export function unverified(message, cause) {
|
|
10
|
+
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
11
11
|
}
|
|
12
12
|
function record(value) {
|
|
13
13
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -46,6 +46,10 @@ function validateDescriptor(value, allowUnknownFlags) {
|
|
|
46
46
|
}
|
|
47
47
|
return value;
|
|
48
48
|
}
|
|
49
|
+
/** Validate policy-free observations crossing the isolated batch transport. */
|
|
50
|
+
export function parseWindowsOwnerAndDaclFacts(value) {
|
|
51
|
+
return validateDescriptor(value, true);
|
|
52
|
+
}
|
|
49
53
|
function summarize(facts) {
|
|
50
54
|
const ownerClass = facts.ownerSid === facts.currentUserSid ? "current-user"
|
|
51
55
|
: facts.ownerSid === SYSTEM_SID ? "system"
|
|
@@ -77,10 +77,11 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
77
77
|
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
78
78
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
79
79
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
80
|
+
| `retainFileInDirectory`, `RetainedFile`, related receipt/result types | [Retained Windows files](retained-file.md) | Existing-file native handle custody and explicit removal; local NTFS, producer authority required, no persistence guarantee. |
|
|
80
81
|
| `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
|
|
81
82
|
| `readDirectoryIdentity`, `assertDirectoryIdentitySync`, `DirectoryIdentity` | [directory-identity.md](directory-identity.md) | Observe exact bigint directory identity and synchronously check a caller-selected path, optionally retaining its canonical path. |
|
|
82
83
|
| `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
|
|
83
|
-
| `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
|
|
84
|
+
| `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks, inspecting raw segments before `..` normalization. A `..` may undo an inspected real directory, but cannot leave the root or undo an allowed root-child symlink. Raw paths outside the root that normalize inside are rejected. |
|
|
84
85
|
| `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
|
|
85
86
|
|
|
86
87
|
`pathExists()` and `pathExistsSync()` intentionally retain ordinary `stat`
|
|
@@ -240,6 +241,7 @@ atomic replacement, use [`Root.write()`](writing.md).
|
|
|
240
241
|
| Export | Page | Notes |
|
|
241
242
|
|---|---|---|
|
|
242
243
|
| `stageFileInDirectory`, `StagedFile`, `StagedFileReceipt`, `PublishedFileReceipt`, `StagedFilePublication`, `StagedFileCleanupReceipt`, `StagedFileFailureDetails` | [staged-file.md](staged-file.md) | Native-required Linux/macOS lifecycle retaining the original directory for abort cleanup. |
|
|
244
|
+
| `retainSymlinkInDirectory`, `StagedSymlink`, `StagedSymlinkExpected`, `StagedSymlinkReceipt`, `PublishedSymlinkReceipt`, `StagedSymlinkPublication`, `StagedSymlinkRemoval`, `StagedSymlinkCleanupReceipt`, `StagedSymlinkFailureDetails` | [staged-symlink.md](staged-symlink.md) | Native-required retained symlink identity, no-replace publication and explicit recovery; never same-target ownership adoption. |
|
|
243
245
|
| `tempFile`, `withTempFile`, `TempFile`, `buildRandomTempFilePath`, `sanitizeTempFileName` | [temp.md](temp.md) | One-file temp primitive; prefer `tempWorkspace` from `@openclaw/fs-safe/temp` for the stable surface. |
|
|
244
246
|
| `writeSiblingTempFile`, `writeViaSiblingTempPath`, `WriteSiblingTempFileOptions`, `WriteSiblingTempFileResult` | – | Callback-produced file staging: verified sibling publication or private-workspace copy through a root. |
|
|
245
247
|
|
|
@@ -256,7 +258,7 @@ atomic replacement, use [`Root.write()`](writing.md).
|
|
|
256
258
|
|---|---|---|
|
|
257
259
|
| `createAsyncLock` | – | In-process async lock (separate from cross-process file locks). |
|
|
258
260
|
| `withTimeout` | [timing.md](timing.md) | Wrap a promise with a timeout that raises `Error` by default, or an error supplied by `createError`. |
|
|
259
|
-
| `movePathToTrash`, `MovePathToTrashOptions` | – | Best-effort move to the platform trash. |
|
|
261
|
+
| `movePathToTrash`, `MovePathToTrashOptions` | – | Best-effort move to the platform trash. Allowed roots constrain the real parent of the moved entry, including symlinks; the referent is not moved. Parent identity is rechecked before mutation. |
|
|
260
262
|
|
|
261
263
|
## Stability
|
|
262
264
|
|
|
@@ -179,6 +179,12 @@ between native and JavaScript paths rather than reimplementing it in Rust.
|
|
|
179
179
|
|
|
180
180
|
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless internal separator and dot-component equivalence is allowed only after validation; every raw and Unicode interpretation must also agree on whether its name ends in `/` or `\`. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
181
181
|
|
|
182
|
+
ZIP preflight and bounded reads apply extraction's case-insensitive NFC collision
|
|
183
|
+
policy to the complete archive, including entries that extraction would strip
|
|
184
|
+
or skip. Known UTF-8 and Unicode Path names are checked during physical admission;
|
|
185
|
+
legacy names retain backend-selected decoding and undergo the same collision
|
|
186
|
+
check before callbacks or selected bytes are returned.
|
|
187
|
+
|
|
182
188
|
ZIP end-record admission searches the bounded comment window for signatures
|
|
183
189
|
while retaining complete comment-length and ambiguity checks. Dense signature
|
|
184
190
|
sequences fall back to the bounded byte scan.
|
|
@@ -211,6 +217,14 @@ loading, including failure. Public preflight still returns ordinary JSZip entry
|
|
|
211
217
|
objects, with directory keys ending in `/` and recognizable symlink type bits.
|
|
212
218
|
Compressed data is retained even for declared-zero entries, so an empty-size
|
|
213
219
|
claim cannot bypass payload-size or CRC verification during extraction or reads.
|
|
220
|
+
Extraction and bounded reads retain the admitted CRC and size independently of
|
|
221
|
+
mutable decoder objects, so later decoder changes cannot redefine the expected
|
|
222
|
+
payload integrity. Public preflight archives retain ordinary JSZip mutation
|
|
223
|
+
and entry-reading behavior.
|
|
224
|
+
Portable bounded reads select the admitted canonical name and reject a selected
|
|
225
|
+
decoder entry that no longer belongs to that archive and name before reading its
|
|
226
|
+
payload. Extraction retains the admitted names and physical order independently
|
|
227
|
+
of later changes to the decoder's public `files` object.
|
|
214
228
|
|
|
215
229
|
Within one ZIP entry, identical local and central name bytes reuse the same
|
|
216
230
|
decoded validation. Unicode Path admission is shared only when both the raw names
|
|
@@ -238,6 +252,8 @@ directory paths. For example, `./pkg//state\cache/value` is presented as
|
|
|
238
252
|
`pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
|
|
239
253
|
spelling are preserved. Local PAX `path`, GNU long-name, and supported ZIP
|
|
240
254
|
Unicode Path names use the same canonicalization.
|
|
255
|
+
A leading `~` is a literal archive entry name and is never expanded to the
|
|
256
|
+
user's home directory during extraction, publication, or durability checks.
|
|
241
257
|
Callbacks follow physical archive order, including ZIP names that look like
|
|
242
258
|
integer object keys. The public ZIP loader's `files` object retains ordinary
|
|
243
259
|
JavaScript object enumeration and mutation behavior.
|
|
@@ -60,6 +60,8 @@ type ReplaceFileAtomicOptions = {
|
|
|
60
60
|
syncTempFile?: boolean; // fsync(temp) before rename, or the final file after copy fallback; default false
|
|
61
61
|
syncParentDir?: boolean; // fsync(parent) after rename, POSIX only; default false
|
|
62
62
|
throwOnCleanupError?: boolean; // report temp cleanup failure; default false
|
|
63
|
+
assertBeforeMutation?: () => void;
|
|
64
|
+
onDestinationState?: (state: ReplaceFileAtomicDestinationState) => void;
|
|
63
65
|
beforeRename?: (params: { filePath: string; tempPath: string }) => Promise<void>;
|
|
64
66
|
fileSystem?: ReplaceFileAtomicFileSystem; // injectable fs for tests
|
|
65
67
|
};
|
|
@@ -108,6 +110,66 @@ descriptor is still closed, and a close failure remains reportable.
|
|
|
108
110
|
|
|
109
111
|
Identity checks and pathname rename/unlink remain separate syscalls, not atomic conditional mutations. Use an approved writable parent plus cooperative locking or OS isolation when arbitrary concurrent namespace mutation is in scope.
|
|
110
112
|
|
|
113
|
+
### Atomic write authority and destination state
|
|
114
|
+
|
|
115
|
+
Use `assertBeforeMutation` when a write depends on a lease or other revocable
|
|
116
|
+
application authority. Both atomic variants capture the callback at entry and
|
|
117
|
+
run it synchronously before directory creation or mode changes, compatibility
|
|
118
|
+
lock acquisition, staging creation and writes, each rename attempt, and fallback
|
|
119
|
+
removal, destination acquisition, truncation, and each content write. Asynchronous destination identity checks
|
|
120
|
+
finish before the final authority check; no await separates that check from the
|
|
121
|
+
write dispatch. A dispatched operation still owns its completion.
|
|
122
|
+
|
|
123
|
+
`onDestinationState` captures facts that a caller may need if the operation later
|
|
124
|
+
rejects:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
type ReplaceFileAtomicDestinationState =
|
|
128
|
+
| Readonly<{ state: "removed"; path: string }>
|
|
129
|
+
| Readonly<{
|
|
130
|
+
state: "writing" | "published";
|
|
131
|
+
path: string;
|
|
132
|
+
dev: bigint;
|
|
133
|
+
ino: bigint;
|
|
134
|
+
}>;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `removed` follows successful removal of an existing fallback destination.
|
|
138
|
+
- `writing` records the fallback's retained descriptor after exclusive creation
|
|
139
|
+
or the first successful truncation of an existing `restore-original` target.
|
|
140
|
+
Opening an existing target leaves it untouched and emits no receipt. This state
|
|
141
|
+
does not claim that all requested bytes were written.
|
|
142
|
+
- `published` follows successful rename and destination identity verification,
|
|
143
|
+
before parent synchronization or descriptor close can fail. Copy fallback
|
|
144
|
+
reports it after its content, mode, and requested file synchronization complete.
|
|
145
|
+
The explicit `verify-content-with-lock` policy first applies its existing
|
|
146
|
+
locked content verification to admit the replacement descriptor.
|
|
147
|
+
|
|
148
|
+
Receipts are frozen and use the destination spelling captured by the operation.
|
|
149
|
+
File identities come from retained descriptors, never from reopening a pathname
|
|
150
|
+
after a failed copy. Failed or indeterminate admission can leave no identity
|
|
151
|
+
receipt; a rename followed by replacement before verification also has no
|
|
152
|
+
publication receipt. Do not infer ownership from a fresh post-failure stat. A later writer
|
|
153
|
+
can replace the pathname, so compare the recorded identity with the current
|
|
154
|
+
entry and recheck application authority before compensation. Receipts do not
|
|
155
|
+
promise durable storage or authorize rollback.
|
|
156
|
+
|
|
157
|
+
Both callbacks must complete synchronously; Promise, thenable, and synchronous
|
|
158
|
+
or asynchronous generator results are rejected. Returned generators are never
|
|
159
|
+
advanced; other ordinary return values are ignored. The first callback refusal
|
|
160
|
+
is terminal, including falsy thrown values; an `EPERM`, `EEXIST`, or `EBUSY` code
|
|
161
|
+
from a callback never starts fallback or retry. Refusal during an in-place
|
|
162
|
+
fallback also stops new restoration writes. Final mode, synchronization, close,
|
|
163
|
+
and private-stage cleanup may still settle against owned identities after
|
|
164
|
+
revocation. Callback refusal does not delete a published destination or a
|
|
165
|
+
competing file at a staging name. An observer that throws must retain its receipt
|
|
166
|
+
first if recovery needs it.
|
|
167
|
+
|
|
168
|
+
`beforeRename` remains the hook for preparing backups. Effects performed inside
|
|
169
|
+
that hook remain the caller's responsibility; the authority option guards the
|
|
170
|
+
atomic writer's own effects. Omitting both new callbacks preserves existing
|
|
171
|
+
write, fallback, restoration, result, and cleanup behavior.
|
|
172
|
+
|
|
111
173
|
### FUSE, Windows exFAT/FAT32, and unstable rename identity
|
|
112
174
|
|
|
113
175
|
Strict source-to-destination identity is the default. Some FUSE mounts assign a different inode to the destination during rename even without concurrency. Set `renameIdentity: "verify-content-with-lock"` to accept that boundary only when the re-opened no-follow destination has the exact requested SHA-256 content under an exclusive hashed sidecar lock in the destination parent. The newly accepted descriptor and identity remain pinned through parent sync and final verification. The synchronous helper provides the same policy with the synchronous lock implementation.
|
|
@@ -184,6 +246,22 @@ that same descriptor, and synchronizes the result. Any write, mode, or sync
|
|
|
184
246
|
failure triggers a byte-and-mode restore and another sync through the same
|
|
185
247
|
descriptor.
|
|
186
248
|
|
|
249
|
+
With mutation callbacks enabled, an `EIO` from destination `stat`/`lstat` after
|
|
250
|
+
successful truncation also attempts restoration through that retained descriptor.
|
|
251
|
+
Each restore write still requires live application authority and fresh exact
|
|
252
|
+
descriptor identity, regular-file, and configured hardlink checks. The failed
|
|
253
|
+
pathname observation is not retried to authorize restoration; no pathname is
|
|
254
|
+
opened, removed, or replaced. A successor at that name is left untouched.
|
|
255
|
+
`details.cleanup: "restored"` means the retained original file's bytes and mode
|
|
256
|
+
were restored and synchronized, not that the pathname still names it. The existing
|
|
257
|
+
`writing` receipt identifies that file; no `published` receipt is emitted for a
|
|
258
|
+
failed replacement. Restoration I/O failures report `"restore-failed"`.
|
|
259
|
+
|
|
260
|
+
Callback refusals, detected identity/type/link changes, and other metadata errors
|
|
261
|
+
remain terminal. Failed descriptor revalidation also stops restoration. These
|
|
262
|
+
cases can leave the retained file empty or partial with only a `writing` receipt;
|
|
263
|
+
before the first successful truncation, verification failure leaves it untouched.
|
|
264
|
+
|
|
187
265
|
With `syncTempFile: false`, an exclusive-create copy fallback does not report
|
|
188
266
|
success until its new destination writer closes successfully. This includes
|
|
189
267
|
`"restore-original"` when the destination did not exist. A close rejection or throw is
|
|
@@ -214,6 +292,10 @@ through short reads and EOF, grow only as data arrives, and enforce the same
|
|
|
214
292
|
`beforeRename` callback, and `ReplaceFileAtomicSyncFileSystem`. Use it inside
|
|
215
293
|
synchronous boot paths or test setup code. It returns the same
|
|
216
294
|
`{ method: "rename" | "copy-fallback" }` receipt as the async variant.
|
|
295
|
+
Promise, thenable, and synchronous or asynchronous generator results from the
|
|
296
|
+
hook reject with `TypeError` before publication; rejected promises are consumed
|
|
297
|
+
and generators are never advanced. Other synchronous return values are ignored.
|
|
298
|
+
The replacement is not published; owned-temp cleanup follows the rules above.
|
|
217
299
|
|
|
218
300
|
## `replaceDirectoryAtomic`
|
|
219
301
|
|
|
@@ -295,9 +377,9 @@ semantics. For single-file replacement, `replaceFileAtomic` is the right tool.
|
|
|
295
377
|
|
|
296
378
|
Atomic UTF-8 text write with the same secure defaults as `writeJson`: sibling
|
|
297
379
|
temp file, descriptor-bound mode setting and fsync, rename, and parent fsync.
|
|
298
|
-
It delegates to `replaceFileAtomic()` with a smaller call shape
|
|
299
|
-
|
|
300
|
-
or custom copy-fallback policy.
|
|
380
|
+
It delegates to `replaceFileAtomic()` with a smaller call shape, including its
|
|
381
|
+
pre-publication hook and staging-prefix options. Use `replaceFileAtomic()` when
|
|
382
|
+
you need mode preservation or a custom copy-fallback policy.
|
|
301
383
|
|
|
302
384
|
```ts
|
|
303
385
|
import { writeTextAtomic } from "@openclaw/fs-safe/atomic";
|
|
@@ -317,9 +399,17 @@ type WriteTextAtomicOptions = {
|
|
|
317
399
|
dirMode?: number; // parent mode (default 0o777 masked by process umask)
|
|
318
400
|
trailingNewline?: boolean; // append "\n" if missing; default false
|
|
319
401
|
durable?: boolean; // default true; false skips temp/parent fsync
|
|
402
|
+
beforeRename?: (params: { filePath: string; tempPath: string }) => Promise<void>;
|
|
403
|
+
tempPrefix?: string; // default ".fs-safe-replace"
|
|
320
404
|
};
|
|
321
405
|
```
|
|
322
406
|
|
|
407
|
+
`beforeRename` is awaited after the complete text is staged and before
|
|
408
|
+
publication, with the same [stage identity and refusal cleanup](#beforerename)
|
|
409
|
+
checks as `replaceFileAtomic`. Pass `tempPrefix` to identify staged files; it
|
|
410
|
+
uses the same prefix validation, including rejection of empty prefixes and path
|
|
411
|
+
separators.
|
|
412
|
+
|
|
323
413
|
`durable: false` keeps the sibling-temp replace/rename behavior but skips the
|
|
324
414
|
temp-file and parent-directory `fsync` calls. Use it only for reconstructible
|
|
325
415
|
metadata where lower latency matters more than crash-durability.
|
|
@@ -70,6 +70,36 @@ Consumers receive the asset in the npm package and need no compiler.
|
|
|
70
70
|
|
|
71
71
|
Output lands in `dist/`. The package's `prepack` hook re-runs the build before publishing — manual `pnpm build` is only required when you want to inspect the output or run a freshly-built copy locally.
|
|
72
72
|
|
|
73
|
+
### Linux GNU release bindings
|
|
74
|
+
|
|
75
|
+
GNU x64 and arm64 artifacts use Zig 0.16.0 and `cargo-zigbuild` 0.23.4 with an
|
|
76
|
+
explicit glibc 2.28 target, independent of the runner's libc. This matches the
|
|
77
|
+
Node Linux runtime baseline and supports RHEL 8-family users without adding a
|
|
78
|
+
second legacy package. Run the same build and ABI gate used in CI and releases:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cargo install cargo-zigbuild --version 0.23.4 --locked
|
|
82
|
+
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
|
|
83
|
+
node scripts/build-linux-gnu.mjs x86_64-unknown-linux-gnu
|
|
84
|
+
node scripts/build-linux-gnu.mjs aarch64-unknown-linux-gnu
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
These commands require Zig on `PATH` and GNU `objdump`. They build the N-API
|
|
88
|
+
cdylib with `cargo zigbuild --target <triple>.2.28`, copy it to the existing
|
|
89
|
+
`artifacts/fs-safe-native.<platform>.node` name, and reject any GLIBC symbol
|
|
90
|
+
requirement above 2.28 before upload. The gate also rejects missing or unknown
|
|
91
|
+
GLIBC versions. To inspect an existing binding, use
|
|
92
|
+
`node scripts/check-linux-glibc.mjs <binding.node>`.
|
|
93
|
+
|
|
94
|
+
`pnpm native:build` remains a host-toolchain development build; it does not
|
|
95
|
+
establish the GNU release ABI floor.
|
|
96
|
+
|
|
97
|
+
On Linux x64 or arm64 with Docker, run `pnpm build`, copy the matching GNU artifact from
|
|
98
|
+
`artifacts/` to `native/`, run `node scripts/stage-host-native.mjs`, then run
|
|
99
|
+
`bash scripts/test-linux-glibc-floor.sh`. CI uses this command to load the actual
|
|
100
|
+
artifact and run native security and no-replace move tests in Rocky Linux 8
|
|
101
|
+
(glibc 2.28). Both GNU architectures execute this load test on matching runners.
|
|
102
|
+
|
|
73
103
|
## Test
|
|
74
104
|
|
|
75
105
|
```bash
|
|
@@ -137,6 +167,52 @@ pnpm check
|
|
|
137
167
|
This runs the filesystem boundary checks, build, tests, and package
|
|
138
168
|
tarball/import validation.
|
|
139
169
|
|
|
170
|
+
The native watch lane requires events on Linux, macOS, and Windows and reports
|
|
171
|
+
actual edit latency. Run `FS_SAFE_TEST_SERIAL=1 pnpm check` to isolate local timing
|
|
172
|
+
checks from the other filesystem stress suites. Watch fixtures use normal OS
|
|
173
|
+
temporary storage; session scratch trees may suppress macOS filesystem events.
|
|
174
|
+
|
|
175
|
+
### Optional Linux Testbox
|
|
176
|
+
|
|
177
|
+
The manual `testbox-validation.yml` workflow prepares a 16-vCPU Ubuntu 24.04
|
|
178
|
+
Blacksmith Testbox with Node 24.21.0, pnpm 12.4.2, dependencies, the Rust WASM
|
|
179
|
+
target, and the pinned portable archive compiler. It leaves library builds and
|
|
180
|
+
validation commands to the caller and does not replace required CI checks.
|
|
181
|
+
|
|
182
|
+
Use an authenticated Blacksmith CLI with access to the repository and its
|
|
183
|
+
Blacksmith organization. From a full repository checkout, warm one session
|
|
184
|
+
through Crabbox:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
CRABBOX_BLACKSMITH_IDLE_TIMEOUT=240m crabbox warmup --provider blacksmith-testbox \
|
|
188
|
+
--blacksmith-org openclaw \
|
|
189
|
+
--blacksmith-workflow .github/workflows/testbox-validation.yml \
|
|
190
|
+
--blacksmith-job validate --blacksmith-ref main \
|
|
191
|
+
--idle-timeout 240m --timing-json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use a branch or tag containing the workflow for `--blacksmith-ref`; GitHub must
|
|
195
|
+
first have registered the workflow on the default branch. The job has a fixed
|
|
196
|
+
240-minute limit. Keep the idle timeout at that limit (240 minutes in the native
|
|
197
|
+
Blacksmith CLI) because the pinned Testbox action can miss active SSH sessions
|
|
198
|
+
behind a forwarded port. Bound commands by the remaining job time and leave time
|
|
199
|
+
to collect results and stop before the deadline.
|
|
200
|
+
|
|
201
|
+
Use the returned `tbx_...` ID for subsequent commands. The `fs-safe-testbox`
|
|
202
|
+
wrapper restores the prepared tool paths and WASM compiler settings in the SSH
|
|
203
|
+
shell. For example:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
crabbox run --provider blacksmith-testbox --id <tbx_id> --timing-json -- \
|
|
207
|
+
fs-safe-testbox pnpm check
|
|
208
|
+
crabbox stop --provider blacksmith-testbox <tbx_id>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Stop the session when finished and verify its terminal status. Blacksmith owns
|
|
212
|
+
checkout synchronization; record the tested source revision or diff, Testbox ID,
|
|
213
|
+
and Actions run. This backend is Linux-only and does not accept Crabbox's direct
|
|
214
|
+
SSH `--script` or `--download` flags. The workflow provides no application secrets.
|
|
215
|
+
|
|
140
216
|
### Method benchmarks
|
|
141
217
|
|
|
142
218
|
`pnpm benchmark:methods` measures the callable library surface against synthetic
|
|
@@ -201,10 +277,32 @@ require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standa
|
|
|
201
277
|
direct `node` invocation without lifecycle metadata is unsupported. Archive
|
|
202
278
|
codecs and their dependencies are packed from the installed dependency graph.
|
|
203
279
|
|
|
204
|
-
PR CI builds and executes
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
280
|
+
PR CI builds and executes all seven shipped bindings. The existing check names
|
|
281
|
+
stay stable; extra runner/runtime combinations add checks without changing the
|
|
282
|
+
repository ruleset. Native lanes run Node 24; the GNU host lanes also exercise
|
|
283
|
+
Bun 1.4.2 (as do macOS and Windows).
|
|
284
|
+
|
|
285
|
+
| Lane | Runner | Architecture / libc | Runtime |
|
|
286
|
+
| --- | --- | --- | --- |
|
|
287
|
+
| JavaScript check | `ubuntu-latest` | x64 / glibc | Node 22, 24, 26 |
|
|
288
|
+
| JavaScript check | `macos-15` | arm64 | Node 22, 24, 26 |
|
|
289
|
+
| JavaScript check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 22, 24, 26 |
|
|
290
|
+
| Native check | `ubuntu-latest` | x64 / glibc | Node 24, Bun 1.4.2 |
|
|
291
|
+
| Native check (forced no-openat2) | `ubuntu-latest` | x64 / glibc | Node 24 |
|
|
292
|
+
| Native check | `ubuntu-24.04-arm` | arm64 / glibc | Node 24, Bun 1.4.2 |
|
|
293
|
+
| Native check | `macos-15` | arm64 | Node 24, Bun 1.4.2 |
|
|
294
|
+
| Native check | `macos-15-intel` | x64 | Node 24, Bun 1.4.2 |
|
|
295
|
+
| Native check | `fs-safe-windows-16core` (`windows-latest`) | x64 | Node 24, Bun 1.4.2 |
|
|
296
|
+
| Native check | `windows-2022` (standard hosted) | x64 | Node 24, Bun 1.4.2 |
|
|
297
|
+
| Native check (Alpine 3.24) | `ubuntu-latest` | x64 / musl | Node 24 |
|
|
298
|
+
| Native check (Alpine 3.24) | `ubuntu-24.04-arm` | arm64 / musl | Node 24 |
|
|
299
|
+
| GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-latest` | x64 / glibc | Node 24 |
|
|
300
|
+
| GNU glibc 2.28 build + Rocky Linux 8 load | `ubuntu-24.04-arm` | arm64 / glibc | Node 24 |
|
|
301
|
+
| Bundled package smoke | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22, 24 |
|
|
302
|
+
| Coverage | `ubuntu-latest`, `macos-15`, `fs-safe-windows-16core` | host | Node 22 |
|
|
303
|
+
|
|
304
|
+
Both musl lanes also run root-only package smoke with the real host binding.
|
|
305
|
+
The seven-target source build matrix still runs on release tags. The smoke writes manager versions,
|
|
208
306
|
cases, and synthetic-fixture scope to `release-artifacts/consumer-proof.json`.
|
|
209
307
|
|
|
210
308
|
## Docs
|
|
@@ -74,6 +74,8 @@ Native Windows byte copies can store large zero-filled chunks as sparse ranges w
|
|
|
74
74
|
|
|
75
75
|
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
76
|
|
|
77
|
+
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
|
+
|
|
77
79
|
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.
|
|
78
80
|
|
|
79
81
|
`readCloneFileMetadata(files)` asynchronously reads APFS data-stream identities and file metadata in one native batch. Results correspond to input order; missing or unsupported entries return `undefined`. The returned `CloneFileMetadata` includes clone ID, device/inode, size, mode, ownership, and timestamps. These are point-in-time observations, not authorization or proof that later reads remain unchanged. Consumers such as Git index adapters must validate their own content and timestamp invariants. The reader does not follow leaf symbolic links.
|
|
@@ -436,3 +436,10 @@ owning product boundary.
|
|
|
436
436
|
- [Migrating to 0.5](migrating-to-0.5.md) — choosing a publication policy during upgrade.
|
|
437
437
|
- [Native architecture](native.md) — clone/copy/hash mechanisms and fallback guarantees.
|
|
438
438
|
- [Errors](errors.md) — typed operational failure handling.
|
|
439
|
+
|
|
440
|
+
## Existing Windows file retirement
|
|
441
|
+
|
|
442
|
+
[`retainFileInDirectory`](retained-file.md) describes identity-bound native
|
|
443
|
+
disposition and resource settlement separately from persistence. Its result is
|
|
444
|
+
always `persistence: "not-proven"`; neither accepted disposition nor observed
|
|
445
|
+
namespace absence is a directory/volume barrier or an application commit.
|
|
@@ -101,6 +101,11 @@ such as `internal space/a b.txt` are accepted. On POSIX, colons elsewhere, such
|
|
|
101
101
|
as the timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
|
|
102
102
|
Windows rejects that spelling as stream syntax.
|
|
103
103
|
|
|
104
|
+
Keys such as `~` and `~/state.json` name literal entries inside the store; they
|
|
105
|
+
do not expand the user's home directory. Reads, writes, removal, and pruning
|
|
106
|
+
all use that literal identity. The `Root` returned by `root()` retains its own
|
|
107
|
+
home-expansion behavior.
|
|
108
|
+
|
|
104
109
|
Validation retains each method's operation order. Async reads, `exists`, and
|
|
105
110
|
`remove` open the root first: if the root is missing, strict methods report
|
|
106
111
|
`not-found` and `readTextIfExists` / `readJsonIfExists` return `null`, even for an
|
|
@@ -119,7 +119,13 @@ publication failure preserves the existing destination and source link; ordinary
|
|
|
119
119
|
failure cleanup removes the staging directory.
|
|
120
120
|
|
|
121
121
|
Cross-device directory moves build a copy manifest and check it during source
|
|
122
|
-
cleanup.
|
|
122
|
+
cleanup. Directory creation keeps the source mode subject to the guest's umask;
|
|
123
|
+
mode `000` is not replaced with a default. A top-level mode-000 directory uses
|
|
124
|
+
owner-only staging until publication, then restores zero through its retained
|
|
125
|
+
descriptor. Reading a mode-000 source still requires sufficient OS privileges;
|
|
126
|
+
the guest does not change source permissions to gain access. A permission error
|
|
127
|
+
after publication preserves the source and published copy for reconciliation.
|
|
128
|
+
Source changes can leave the published destination and some or all
|
|
123
129
|
of the source. Regular-file and symlink move fallbacks unlink the source
|
|
124
130
|
pathname after publication; they do not perform the directory manifest's
|
|
125
131
|
identity checks. Directory cleanup also has check-to-unlink race windows.
|
|
@@ -65,6 +65,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
65
65
|
| [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for private JSON/text state at 0600 under 0700 dirs. |
|
|
66
66
|
| [`tempWorkspace`](temp.md) | 0700 scratch dir with auto-cleanup. |
|
|
67
67
|
| [`readSecureFile`](secure-file.md) | Absolute file reads with fd pinning, permissions, owner, size, and timeout checks. |
|
|
68
|
+
| [`watch`](watch.md) | Guarded filesystem observation with advisory native hints and polling. |
|
|
68
69
|
| [`walkDirectory` / `Root.walk`](walk.md) | Standalone inventories plus root-bounded pruning, budgets, and partial-error reporting. |
|
|
69
70
|
| [`Root.entries`](entries.md) | Guarded nonrecursive entries, bounded name collection, and caller-owned symlink validation. |
|
|
70
71
|
| [`extractArchive`](archive.md) | Policy-driven ZIP/TAR extraction with clamp/filter, metadata/path-depth, link, count, and byte limits. |
|