@openclaw/openshell-sandbox 2026.9.6 → 2026.9.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/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
|
@@ -4,10 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync, } from "./directory-guard.js";
|
|
5
5
|
import { admitTempWorkspaceChild, admitTempWorkspaceChildSync, assertTrustedTempWorkspaceDirectory, inspectTempWorkspaceDescriptorIdentitySync, projectTempWorkspaceNumericIdentity, TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY, } from "./temp-workspace-child-admission.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
8
7
|
import { realpathSync } from "./realpath.js";
|
|
9
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
10
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
11
10
|
const WINDOWS = process.platform === "win32";
|
|
12
11
|
function effectiveOwner() {
|
|
13
12
|
// Windows mode/uid fields do not describe ACL authority. The supplied root's
|
|
@@ -37,20 +36,21 @@ function copyExactDirectoryObservation(stat) {
|
|
|
37
36
|
dev: stat.dev,
|
|
38
37
|
ino: stat.ino,
|
|
39
38
|
uid: stat.uid,
|
|
39
|
+
gid: stat.gid,
|
|
40
40
|
mode: stat.mode,
|
|
41
41
|
directory: stat.isDirectory(),
|
|
42
42
|
symbolicLink: stat.isSymbolicLink(),
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
function snapshotFromExactObservation(dir, uid, realPath, stat) {
|
|
45
|
+
function snapshotFromExactObservation(dir, uid, realPath, stat, privateDirectory = false) {
|
|
46
46
|
if (stat.symbolicLink || !stat.directory) {
|
|
47
47
|
throw new FsSafeError("not-file", "temp workspace root component must be a real directory");
|
|
48
48
|
}
|
|
49
|
-
assertTrustedTempWorkspaceDirectory(stat, uid);
|
|
49
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory);
|
|
50
50
|
const identity = Object.freeze({ dev: stat.dev, ino: stat.ino });
|
|
51
51
|
// Retain copied immutable scalars, never a mutable Stats object supplied by
|
|
52
52
|
// an observation hook. Every later permission check uses a fresh snapshot.
|
|
53
|
-
return Object.freeze({ dir, identity, numericIdentity: safeNumericIdentity(stat), realPath });
|
|
53
|
+
return Object.freeze({ dir, identity, numericIdentity: safeNumericIdentity(stat), realPath, privateDirectory });
|
|
54
54
|
}
|
|
55
55
|
function canonicalTempWorkspacePath(dir) {
|
|
56
56
|
assertNoWindowsPathAlias(dir, "filesystem");
|
|
@@ -58,10 +58,10 @@ function canonicalTempWorkspacePath(dir) {
|
|
|
58
58
|
assertNoWindowsPathAlias(canonical, "filesystem");
|
|
59
59
|
return canonical;
|
|
60
60
|
}
|
|
61
|
-
function snapshot(dir, uid, realPath = canonicalTempWorkspacePath(dir)) {
|
|
61
|
+
function snapshot(dir, uid, realPath = canonicalTempWorkspacePath(dir), privateDirectory = false) {
|
|
62
62
|
const stat = inspectDirectoryIdentitySync(dir);
|
|
63
63
|
const observation = copyExactDirectoryObservation(stat);
|
|
64
|
-
return { entry: snapshotFromExactObservation(dir, uid, realPath, observation), stat };
|
|
64
|
+
return { entry: snapshotFromExactObservation(dir, uid, realPath, observation, privateDirectory), stat };
|
|
65
65
|
}
|
|
66
66
|
function hasCompleteExactIdentity(stat) {
|
|
67
67
|
return typeof stat.dev === "bigint" && typeof stat.ino === "bigint" &&
|
|
@@ -72,11 +72,6 @@ function assertCanonicalRoot(entry) {
|
|
|
72
72
|
throw new FsSafeError("path-mismatch", "temp workspace root ancestry changed");
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
function identityMismatch() {
|
|
76
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
77
|
-
recordFileObservationFailure(error, "identity");
|
|
78
|
-
throw error;
|
|
79
|
-
}
|
|
80
75
|
function inspectSnapshotIdentity(entry) {
|
|
81
76
|
const expected = entry.numericIdentity;
|
|
82
77
|
if (!expected) {
|
|
@@ -87,21 +82,21 @@ function inspectSnapshotIdentity(entry) {
|
|
|
87
82
|
const devKnown = Number.isSafeInteger(stat.dev) && stat.dev >= 0 && (!WINDOWS || stat.dev !== 0);
|
|
88
83
|
if (devKnown) {
|
|
89
84
|
if (stat.dev !== expected.dev)
|
|
90
|
-
|
|
85
|
+
throw fileIdentityMismatchError();
|
|
91
86
|
}
|
|
92
87
|
else if (WINDOWS)
|
|
93
88
|
requiresExactRetry = true;
|
|
94
89
|
else
|
|
95
|
-
|
|
90
|
+
throw fileIdentityMismatchError();
|
|
96
91
|
const inoKnown = Number.isSafeInteger(stat.ino) && stat.ino >= 0 && (!WINDOWS || stat.ino !== 0);
|
|
97
92
|
if (inoKnown) {
|
|
98
93
|
if (stat.ino !== expected.ino)
|
|
99
|
-
|
|
94
|
+
throw fileIdentityMismatchError();
|
|
100
95
|
}
|
|
101
96
|
else if (WINDOWS)
|
|
102
97
|
requiresExactRetry = true;
|
|
103
98
|
else
|
|
104
|
-
|
|
99
|
+
throw fileIdentityMismatchError();
|
|
105
100
|
if (!requiresExactRetry)
|
|
106
101
|
return stat;
|
|
107
102
|
// Read exact identity only once. The strict helper may re-check this constant
|
|
@@ -111,16 +106,16 @@ function inspectSnapshotIdentity(entry) {
|
|
|
111
106
|
}
|
|
112
107
|
function assertSnapshot(entry, uid) {
|
|
113
108
|
const stat = inspectSnapshotIdentity(entry);
|
|
114
|
-
assertTrustedTempWorkspaceDirectory(stat, uid);
|
|
109
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory);
|
|
115
110
|
assertCanonicalRoot(entry);
|
|
116
111
|
}
|
|
117
|
-
function snapshotMissingRootComponent(parent, dir, ownerUid) {
|
|
112
|
+
function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
118
113
|
if (parent.dir !== parent.realPath) {
|
|
119
114
|
assertSnapshot(parent, ownerUid);
|
|
120
|
-
return snapshot(dir, ownerUid);
|
|
115
|
+
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
121
116
|
}
|
|
122
117
|
const current = inspectSnapshotIdentity(parent);
|
|
123
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid);
|
|
118
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory);
|
|
124
119
|
let realPath;
|
|
125
120
|
try {
|
|
126
121
|
realPath = canonicalTempWorkspacePath(dir);
|
|
@@ -132,16 +127,16 @@ function snapshotMissingRootComponent(parent, dir, ownerUid) {
|
|
|
132
127
|
}
|
|
133
128
|
if (path.dirname(realPath) !== parent.realPath) {
|
|
134
129
|
assertCanonicalRoot(parent);
|
|
135
|
-
return snapshot(dir, ownerUid);
|
|
130
|
+
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
136
131
|
}
|
|
137
132
|
// The child's canonical parent confirms the same parent name at this phase.
|
|
138
133
|
// Its exact non-symlink/owner observation still precedes mode initialization.
|
|
139
|
-
return snapshot(dir, ownerUid, realPath);
|
|
134
|
+
return snapshot(dir, ownerUid, realPath, privateDirectory);
|
|
140
135
|
}
|
|
141
136
|
function assertChain(chain, uid) {
|
|
142
137
|
for (const entry of chain) {
|
|
143
138
|
const current = inspectSnapshotIdentity(entry);
|
|
144
|
-
assertTrustedTempWorkspaceDirectory(current, uid);
|
|
139
|
+
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory);
|
|
145
140
|
}
|
|
146
141
|
const last = chain[chain.length - 1];
|
|
147
142
|
assertCanonicalRoot(last);
|
|
@@ -157,14 +152,14 @@ function canonicalAncestry(root) {
|
|
|
157
152
|
}
|
|
158
153
|
function exactIdentityMatches(current, expected) {
|
|
159
154
|
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
160
|
-
|
|
155
|
+
throw fileIdentityMismatchError();
|
|
161
156
|
}
|
|
162
157
|
function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
|
|
163
158
|
const stat = inspectTempWorkspaceDescriptorIdentitySync(descriptorFd, entry.identity, entry.numericIdentity);
|
|
164
159
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
165
160
|
throw new FsSafeError("not-file", "temp workspace cleanup parent must be a real directory");
|
|
166
161
|
}
|
|
167
|
-
assertTrustedTempWorkspaceDirectory(stat, ownerUid);
|
|
162
|
+
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory);
|
|
168
163
|
}
|
|
169
164
|
function discoverMissingTempWorkspaceAncestor(ancestor, missing, missingError) {
|
|
170
165
|
// The caller already classified the first ENOENT. Keep this handoff out of
|
|
@@ -225,14 +220,14 @@ function rootPlan(rootDir) {
|
|
|
225
220
|
const existingCanonicalRoot = missing.length === 0 && observedAncestor === ancestor &&
|
|
226
221
|
!initial.symbolicLink && initial.directory && hasCompleteExactIdentity(initial);
|
|
227
222
|
if (existingCanonicalRoot) {
|
|
228
|
-
const discovery = snapshotFromExactObservation(ancestor, ownerUid, ancestor, initial);
|
|
223
|
+
const discovery = snapshotFromExactObservation(ancestor, ownerUid, ancestor, initial, true);
|
|
229
224
|
return {
|
|
230
225
|
admission: canonicalRootAdmission(discovery, ownerUid),
|
|
231
226
|
missing,
|
|
232
227
|
ownerUid,
|
|
233
228
|
};
|
|
234
229
|
}
|
|
235
|
-
const chain = canonicalAncestry(ancestor).map((dir) => snapshot(dir, ownerUid, dir).entry);
|
|
230
|
+
const chain = canonicalAncestry(ancestor).map((dir) => snapshot(dir, ownerUid, dir, missing.length === 0 && dir === ancestor).entry);
|
|
236
231
|
// Missing-component creation needs a replay before its first mutation. When
|
|
237
232
|
// the complete root already exists through an alias, retain the historical
|
|
238
233
|
// guarded route because discovery and mutation use different path spellings.
|
|
@@ -292,10 +287,10 @@ function canonicalRootAdmission(discovery, ownerUid) {
|
|
|
292
287
|
const ancestry = canonicalAncestry(discovery.dir);
|
|
293
288
|
const candidate = ancestry.map((dir, index) => {
|
|
294
289
|
if (!TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY || index !== ancestry.length - 1) {
|
|
295
|
-
return snapshot(dir, ownerUid, dir).entry;
|
|
290
|
+
return snapshot(dir, ownerUid, dir, index === ancestry.length - 1 && discovery.privateDirectory).entry;
|
|
296
291
|
}
|
|
297
292
|
const current = inspectSnapshotIdentity(discovery);
|
|
298
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid);
|
|
293
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory);
|
|
299
294
|
return discovery;
|
|
300
295
|
});
|
|
301
296
|
const current = candidate[candidate.length - 1];
|
|
@@ -338,7 +333,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
338
333
|
if (admission)
|
|
339
334
|
return admission;
|
|
340
335
|
const guardedChain = chain;
|
|
341
|
-
for (const segment of missing) {
|
|
336
|
+
for (const [index, segment] of missing.entries()) {
|
|
342
337
|
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
343
338
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
344
339
|
const dir = path.join(parent.dir, segment);
|
|
@@ -353,7 +348,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
353
348
|
if (error.code !== "EEXIST")
|
|
354
349
|
throw error;
|
|
355
350
|
}
|
|
356
|
-
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
351
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid, index === missing.length - 1);
|
|
357
352
|
if (created) {
|
|
358
353
|
const modeInitialization = admitTempWorkspaceChild(dir, observed.stat, parent, 0o700);
|
|
359
354
|
if (modeInitialization)
|
|
@@ -368,7 +363,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
368
363
|
if (admission)
|
|
369
364
|
return admission;
|
|
370
365
|
const guardedChain = chain;
|
|
371
|
-
for (const segment of missing) {
|
|
366
|
+
for (const [index, segment] of missing.entries()) {
|
|
372
367
|
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
373
368
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
374
369
|
const dir = path.join(parent.dir, segment);
|
|
@@ -383,7 +378,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
383
378
|
if (error.code !== "EEXIST")
|
|
384
379
|
throw error;
|
|
385
380
|
}
|
|
386
|
-
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
381
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid, index === missing.length - 1);
|
|
387
382
|
if (created)
|
|
388
383
|
admitTempWorkspaceChildSync(dir, observed.stat, parent, 0o700);
|
|
389
384
|
guardedChain.push(observed.entry);
|
|
@@ -14,7 +14,7 @@ export declare function projectTempWorkspaceNumericIdentity(identity: TempWorksp
|
|
|
14
14
|
export declare function inspectTempWorkspaceDescriptorIdentitySync(fd: number, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
15
15
|
export declare function inspectTempWorkspaceDirectoryIdentitySync(dir: string, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
16
16
|
export declare function validateTempWorkspaceDirMode(mode: number): void;
|
|
17
|
-
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "mode"> | Pick<Stats, "uid" | "mode">, uid: number | undefined,
|
|
17
|
+
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory?: boolean): void;
|
|
18
18
|
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined): void;
|
|
19
19
|
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number): boolean;
|
|
20
20
|
export declare function childHasRequestedMode(stat: BigIntStats | Stats, mode: number): boolean;
|
|
@@ -2,8 +2,8 @@ import fsSync, {} from "node:fs";
|
|
|
2
2
|
import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./directory-guard.js";
|
|
3
3
|
import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
import { classifyTempWorkspaceOwner, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
|
|
7
7
|
export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
|
|
8
8
|
export function projectTempWorkspaceNumericIdentity(identity) {
|
|
9
9
|
const dev = Number(identity.dev);
|
|
@@ -14,15 +14,10 @@ export function projectTempWorkspaceNumericIdentity(identity) {
|
|
|
14
14
|
}
|
|
15
15
|
return Object.freeze({ dev, ino });
|
|
16
16
|
}
|
|
17
|
-
function identityMismatch() {
|
|
18
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
19
|
-
recordFileObservationFailure(error, "identity");
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
17
|
function inspectNumericIdentity(current, expected) {
|
|
23
18
|
if (!Number.isSafeInteger(current.dev) || current.dev < 0 || current.dev !== expected.dev ||
|
|
24
19
|
!Number.isSafeInteger(current.ino) || current.ino < 0 || current.ino !== expected.ino) {
|
|
25
|
-
|
|
20
|
+
throw fileIdentityMismatchError();
|
|
26
21
|
}
|
|
27
22
|
return current;
|
|
28
23
|
}
|
|
@@ -48,16 +43,16 @@ export function validateTempWorkspaceDirMode(mode) {
|
|
|
48
43
|
throw new FsSafeError("insecure-permissions", "temp workspace must not be group/world writable");
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
|
-
export function assertTrustedTempWorkspaceDirectory(stat, uid,
|
|
46
|
+
export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory = false) {
|
|
52
47
|
if (uid === undefined)
|
|
53
48
|
return;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new FsSafeError("not-owned", "temp workspace directory has an untrusted owner");
|
|
49
|
+
const ownership = classifyTempWorkspaceOwner(stat, uid);
|
|
50
|
+
if (ownership === "foreign" || (privateDirectory && ownership !== "user")) {
|
|
51
|
+
throw new FsSafeError("not-owned", "temp workspace directory has an untrusted owner; use a private temp root owned by the effective user under a trusted directory hierarchy");
|
|
58
52
|
}
|
|
59
|
-
//
|
|
60
|
-
//
|
|
53
|
+
// Unmapped host owners are unverifiable; trust the host directory hierarchy.
|
|
54
|
+
// Admit sticky ancestors so PrivateUsers keeps host /tmp and PrivateTmp usable.
|
|
55
|
+
// Leaf roots stay euid-owned/private; non-sticky writable ancestors are rejected.
|
|
61
56
|
const writable = typeof stat.mode === "bigint"
|
|
62
57
|
? (stat.mode & 18n) !== 0n
|
|
63
58
|
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o022) !== 0;
|
|
@@ -67,9 +62,13 @@ export function assertTrustedTempWorkspaceDirectory(stat, uid, child = false) {
|
|
|
67
62
|
if (typeof stat.mode !== "bigint" && (!Number.isSafeInteger(stat.mode) || stat.mode < 0)) {
|
|
68
63
|
throw new FsSafeError("insecure-permissions", "temp workspace directory permissions are invalid");
|
|
69
64
|
}
|
|
70
|
-
if (writable && (
|
|
71
|
-
throw new FsSafeError("insecure-permissions",
|
|
65
|
+
if (writable && (privateDirectory || !sticky)) {
|
|
66
|
+
throw new FsSafeError("insecure-permissions", privateDirectory
|
|
67
|
+
? "temp workspace root and child must not be group/world writable; use a private temp directory"
|
|
68
|
+
: "temp workspace ancestor is group/world writable without sticky protection");
|
|
72
69
|
}
|
|
70
|
+
if (ownership === "unmapped")
|
|
71
|
+
warnUnmappedTempWorkspaceAncestor();
|
|
73
72
|
}
|
|
74
73
|
const WINDOWS = process.platform === "win32";
|
|
75
74
|
export function assertTempWorkspaceChildState(stat, ownerUid) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import { nodeDirectorySearchOnlyFlags } from "./directory-mode-node.js";
|
|
4
|
-
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
3
|
+
import { nodeDirectorySearchOnlyFlags, assertOwnedDirectory } from "./directory-mode-node.js";
|
|
5
4
|
import { FsSafeError } from "./errors.js";
|
|
6
5
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
7
6
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
@@ -14,13 +14,6 @@ function isNativeCleanupBinding(binding) {
|
|
|
14
14
|
typeof binding.removeOwnedTreeSync === "function" &&
|
|
15
15
|
typeof binding.ownedTreeRemovalAvailable === "function";
|
|
16
16
|
}
|
|
17
|
-
function nativeRemovalError(result) {
|
|
18
|
-
if (!result.errorCode)
|
|
19
|
-
return undefined;
|
|
20
|
-
return Object.assign(new Error(result.errorMessage ?? "native owned-tree cleanup failed"), {
|
|
21
|
-
code: result.errorCode,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
17
|
export class TempWorkspaceCleanupCapability {
|
|
25
18
|
binding;
|
|
26
19
|
parent;
|
|
@@ -309,8 +302,10 @@ export class TempWorkspaceCleanupOwner {
|
|
|
309
302
|
this.#capability.assertCurrent();
|
|
310
303
|
}
|
|
311
304
|
#mapRemoval(result) {
|
|
312
|
-
|
|
313
|
-
|
|
305
|
+
if (result.errorCode) {
|
|
306
|
+
const error = Object.assign(new Error(result.errorMessage ?? "native owned-tree cleanup failed"), {
|
|
307
|
+
code: result.errorCode,
|
|
308
|
+
});
|
|
314
309
|
if (error.code === "path-mismatch")
|
|
315
310
|
return "indeterminate";
|
|
316
311
|
throw error;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type DirectoryOwner = {
|
|
2
|
+
uid: number | bigint;
|
|
3
|
+
gid: number | bigint;
|
|
4
|
+
};
|
|
5
|
+
export type TempWorkspaceOwnership = "user" | "root" | "unmapped" | "foreign";
|
|
6
|
+
export declare function classifyTempWorkspaceOwner(stat: DirectoryOwner, uid: number): TempWorkspaceOwnership;
|
|
7
|
+
export declare function warnUnmappedTempWorkspaceAncestor(): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
let warned = false;
|
|
3
|
+
function overflowId(kind) {
|
|
4
|
+
try {
|
|
5
|
+
const value = Number(fs.readFileSync(`/proc/sys/kernel/overflow${kind}`, "utf8").trim());
|
|
6
|
+
if (Number.isSafeInteger(value) && value >= 0 && value < 0xffffffff)
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Linux defaults when the sysctl files are unavailable in the service mount.
|
|
11
|
+
}
|
|
12
|
+
return 65534;
|
|
13
|
+
}
|
|
14
|
+
function unmappedId(kind) {
|
|
15
|
+
const mappings = fs.readFileSync(`/proc/self/${kind}_map`, "utf8").trim()
|
|
16
|
+
.split("\n").map((line) => line.trim().split(/\s+/).map(Number));
|
|
17
|
+
if (!mappings.every((row) => row.length === 3 && row.every((id) => Number.isSafeInteger(id) && id >= 0 && id <= 0xffffffff) && row[2] > 0))
|
|
18
|
+
return null;
|
|
19
|
+
if (mappings.length === 1 && mappings[0][0] === 0 &&
|
|
20
|
+
mappings[0][1] === 0 && mappings[0][2] === 0xffffffff)
|
|
21
|
+
return null;
|
|
22
|
+
const id = overflowId(kind);
|
|
23
|
+
// Recheck every admission: an explicitly mapped overflow-number ID is real ownership.
|
|
24
|
+
return mappings.some(([start, , count]) => id >= start && id < start + count) ? null : id;
|
|
25
|
+
}
|
|
26
|
+
export function classifyTempWorkspaceOwner(stat, uid) {
|
|
27
|
+
if (stat.uid === uid || stat.uid === BigInt(uid))
|
|
28
|
+
return "user";
|
|
29
|
+
if (stat.uid === 0 || stat.uid === 0n)
|
|
30
|
+
return "root";
|
|
31
|
+
if (process.platform !== "linux")
|
|
32
|
+
return "foreign";
|
|
33
|
+
try {
|
|
34
|
+
const overflowUid = unmappedId("uid");
|
|
35
|
+
const overflowGid = unmappedId("gid");
|
|
36
|
+
return overflowUid !== null && overflowGid !== null &&
|
|
37
|
+
(stat.uid === overflowUid || stat.uid === BigInt(overflowUid)) &&
|
|
38
|
+
(stat.gid === overflowGid || stat.gid === BigInt(overflowGid))
|
|
39
|
+
? "unmapped"
|
|
40
|
+
: "foreign";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Unavailable mapping evidence cannot authorize the namespace exception.
|
|
44
|
+
return "foreign";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function warnUnmappedTempWorkspaceAncestor() {
|
|
48
|
+
if (warned)
|
|
49
|
+
return;
|
|
50
|
+
warned = true;
|
|
51
|
+
process.emitWarning("Temp workspace ancestor ownership is unverifiable in this user namespace; trusting the host directory hierarchy while enforcing ancestor modes and a process-owned private root.", { code: "FS_SAFE_UNMAPPED_TEMP_ANCESTOR", type: "FsSafeWarning" });
|
|
52
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
3
|
export type FsSafeTestHooks = {
|
|
4
|
+
beforeWatchRegistration?: (path: string) => void | Promise<void>;
|
|
5
|
+
afterWatchRegistration?: (path: string) => void | Promise<void>;
|
|
6
|
+
afterWatchBackendOverflow?: (root: string, phase: "received" | "reconciled") => void;
|
|
7
|
+
afterWatchBackendCreated?: (root: string, emit: (batch: import("./watch-native.js").NativeWatchBatch) => void, nativeEvent?: (path: string, flags: number) => void) => void;
|
|
4
8
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
5
9
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
6
10
|
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
@@ -25,7 +29,7 @@ export type FsSafeTestHooks = {
|
|
|
25
29
|
beforeTempWorkspaceNativeRemovalSync?: (quarantinePath: string) => void;
|
|
26
30
|
beforeTrashMove?: (targetPath: string, destPath: string) => void;
|
|
27
31
|
afterPublishTargetCreated?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
|
|
28
|
-
beforePublishDirectorySync?:
|
|
32
|
+
beforePublishDirectorySync?: NonNullable<FsSafeTestHooks["afterPublishTargetCreated"]>;
|
|
29
33
|
};
|
|
30
34
|
export declare function getFsSafeTestHooks(): FsSafeTestHooks | undefined;
|
|
31
35
|
export declare function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ReplaceFileAtomicOptions } from "./replace-file.js";
|
|
2
|
+
export type WriteTextAtomicOptions = Pick<ReplaceFileAtomicOptions, "beforeRename" | "tempPrefix"> & {
|
|
2
3
|
mode?: number;
|
|
3
4
|
dirMode?: number;
|
|
4
5
|
trailingNewline?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertSyncDirectoryGuard, createSyncDirectoryGuard } from "./directory-guard.js";
|
|
4
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
6
|
import { guardedRenameSync, guardedRmSync } from "./guarded-mutation.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
@@ -72,6 +73,22 @@ function resolveTrashTargetPath(targetPath) {
|
|
|
72
73
|
assertNoTrashPathAlias(resolvedPath, "target path");
|
|
73
74
|
return { path: resolvedPath, resolved: true };
|
|
74
75
|
}
|
|
76
|
+
function resolveTrashEntryParent(lexicalTarget, targetPath) {
|
|
77
|
+
const lexicalParent = path.dirname(lexicalTarget);
|
|
78
|
+
assertNoTrashPathAlias(lexicalParent, "target path");
|
|
79
|
+
let realParent;
|
|
80
|
+
try {
|
|
81
|
+
// The renamed name lives in this parent. rename follows intermediate
|
|
82
|
+
// symlinks, so a lexical parent inside an allowed root is not enough.
|
|
83
|
+
realParent = realpathSync.native(lexicalParent);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
87
|
+
}
|
|
88
|
+
const resolvedParent = path.resolve(realParent);
|
|
89
|
+
assertNoTrashPathAlias(resolvedParent, "target path");
|
|
90
|
+
return resolvedParent;
|
|
91
|
+
}
|
|
75
92
|
function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
76
93
|
assertNoTrashPathAlias(targetPath, "target path");
|
|
77
94
|
const lexicalTarget = path.resolve(targetPath);
|
|
@@ -79,11 +96,19 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
79
96
|
const stat = fs.lstatSync(lexicalTarget);
|
|
80
97
|
const resolvedTarget = resolveTrashTargetPath(targetPath);
|
|
81
98
|
const resolvedTargetPath = resolvedTarget.path;
|
|
82
|
-
const
|
|
99
|
+
const parent = createSyncDirectoryGuard(path.dirname(lexicalTarget));
|
|
100
|
+
// Admit the directory entry only when its parent really stays inside an
|
|
101
|
+
// allowed root. Do not admit it because the symlink target is inside.
|
|
102
|
+
const resolvedParent = resolveTrashEntryParent(lexicalTarget, targetPath);
|
|
103
|
+
const isAllowed = resolveAllowedTrashRoots(allowedRoots).some((root) => isSameOrChildPath(resolvedParent, root));
|
|
83
104
|
if (!isAllowed) {
|
|
84
105
|
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
85
106
|
}
|
|
107
|
+
// Sync and native realpath can use different Windows short-name spellings.
|
|
108
|
+
// Recheck the retained guard around native containment instead of comparing them.
|
|
109
|
+
assertSyncDirectoryGuard(parent);
|
|
86
110
|
return {
|
|
111
|
+
parent,
|
|
87
112
|
path: lexicalTarget,
|
|
88
113
|
realPath: resolvedTargetPath,
|
|
89
114
|
realPathResolved: resolvedTarget.resolved,
|
|
@@ -91,6 +116,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
91
116
|
};
|
|
92
117
|
}
|
|
93
118
|
function assertTrashTargetGuard(guard) {
|
|
119
|
+
assertSyncDirectoryGuard(guard.parent);
|
|
94
120
|
const stat = fs.lstatSync(guard.path);
|
|
95
121
|
if (!sameFileIdentity(stat, guard.stat)) {
|
|
96
122
|
throw new Error(`Refusing to trash path after it changed: ${guard.path}`);
|
|
@@ -20,12 +20,9 @@ export type AsyncWalkDirectoryOptions = Omit<WalkDirectoryOptions, "include" | "
|
|
|
20
20
|
include?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
21
21
|
descend?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
22
22
|
};
|
|
23
|
-
export type WalkDirectoryFailure = {
|
|
24
|
-
path: string;
|
|
25
|
-
relativePath: string;
|
|
26
|
-
depth: number;
|
|
23
|
+
export type WalkDirectoryFailure = Pick<WalkDirectoryEntry & {
|
|
27
24
|
error: unknown;
|
|
28
|
-
}
|
|
25
|
+
}, "path" | "relativePath" | "depth" | "error">;
|
|
29
26
|
export type WalkDirectoryResult = {
|
|
30
27
|
entries: WalkDirectoryEntry[];
|
|
31
28
|
scannedEntryCount: number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type RootContext } from "./root-context.js";
|
|
2
|
+
import type { NativeWatchBatch } from "./watch-native.js";
|
|
3
|
+
import type { WatchSnapshot } from "./watch-scan.js";
|
|
4
|
+
import type { WatchChange, WatchScope } from "./watch-types.js";
|
|
5
|
+
/** Resolve native spelling aliases without treating case folding as identity. */
|
|
6
|
+
export declare function admittedNativeChanges(root: RootContext, scopes: readonly WatchScope[], before: WatchSnapshot | undefined, after: WatchSnapshot, batch: NativeWatchBatch, signal: AbortSignal, limit: number): Promise<WatchChange[] | undefined>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { isNotFoundPathError } from "./path.js";
|
|
4
|
+
import { assertRootIdentityCurrent, resolvePathInRoot } from "./root-context.js";
|
|
5
|
+
import { createRootDirectoryObservationGuard, assertRootDirectoryObservationGuard } from "./root-directory-list.js";
|
|
6
|
+
import { lookupRootDirectoryEntry } from "./root-directory-entry.js";
|
|
7
|
+
import { excludedWatchPath, nativeChanges, scopedChanges } from "./watch-hints.js";
|
|
8
|
+
/** Resolve native spelling aliases without treating case folding as identity. */
|
|
9
|
+
export async function admittedNativeChanges(root, scopes, before, after, batch, signal, limit) {
|
|
10
|
+
if (!nativeChanges(scopes, before, batch, limit, after))
|
|
11
|
+
return undefined;
|
|
12
|
+
const result = new Map();
|
|
13
|
+
const candidates = new Map([...before?.targets ?? [], ...after.targets, ...after.directories]);
|
|
14
|
+
const add = (change) => {
|
|
15
|
+
if (!result.has(change.path) && result.size >= limit)
|
|
16
|
+
return false;
|
|
17
|
+
const prior = result.get(change.path);
|
|
18
|
+
result.set(change.path, prior?.type === "structural" ? prior : change);
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
for (const hint of batch.hints) {
|
|
22
|
+
signal.throwIfAborted();
|
|
23
|
+
const name = hint.name; // nativeChanges rejected unknown or non-literal names.
|
|
24
|
+
let parent = hint.directory;
|
|
25
|
+
const relative = parent ? path.join(parent, name) : name;
|
|
26
|
+
if (excludedWatchPath(before, relative) || excludedWatchPath(after, relative))
|
|
27
|
+
continue;
|
|
28
|
+
let guard;
|
|
29
|
+
let expected = after.directories.get(parent);
|
|
30
|
+
if (!expected) {
|
|
31
|
+
// Native recursion observes one Root handle and may report descendants of
|
|
32
|
+
// unselected/entry-only directories. Admit a parent alias by exact identity,
|
|
33
|
+
// not by lowercasing, and do not turn unselected descendants into events.
|
|
34
|
+
try {
|
|
35
|
+
const resolved = await resolvePathInRoot(root, parent ? "./" + parent : ".", { rejectSymlinks: true });
|
|
36
|
+
guard = await createRootDirectoryObservationGuard(root, resolved.resolved);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
await assertRootIdentityCurrent(root);
|
|
40
|
+
if (isNotFoundPathError(error) || (error instanceof FsSafeError && ["not-found", "path-alias", "outside-workspace", "symlink", "not-file"].includes(error.code)))
|
|
41
|
+
continue;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const admitted = [...after.directories].find(([, identity]) => identity.dev === guard.stat.dev && identity.ino === guard.stat.ino);
|
|
45
|
+
if (!admitted)
|
|
46
|
+
continue;
|
|
47
|
+
[parent, expected] = admitted;
|
|
48
|
+
}
|
|
49
|
+
const candidate = parent ? path.join(parent, name) : name;
|
|
50
|
+
if (excludedWatchPath(before, candidate) || excludedWatchPath(after, candidate))
|
|
51
|
+
continue;
|
|
52
|
+
const selected = scopedChanges(scopes, { path: candidate,
|
|
53
|
+
type: hint.event === "change" && before?.entries.get(candidate)?.startsWith("file:") ? "content" : "structural" });
|
|
54
|
+
if (selected.length) {
|
|
55
|
+
for (const change of selected)
|
|
56
|
+
if (!add(change))
|
|
57
|
+
return undefined;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!guard) {
|
|
61
|
+
const resolved = await resolvePathInRoot(root, parent ? "./" + parent : ".", { rejectSymlinks: true });
|
|
62
|
+
guard = await createRootDirectoryObservationGuard(root, resolved.resolved);
|
|
63
|
+
}
|
|
64
|
+
if (guard.stat.dev !== expected.dev || guard.stat.ino !== expected.ino) {
|
|
65
|
+
throw new FsSafeError("path-mismatch", "watch hint parent changed during reconciliation");
|
|
66
|
+
}
|
|
67
|
+
const found = await lookupRootDirectoryEntry(root, guard, name);
|
|
68
|
+
signal.throwIfAborted();
|
|
69
|
+
// A missing unselected sibling is not evidence of lost selected detail.
|
|
70
|
+
// Previously observed selected deletions remain visible in the snapshot diff;
|
|
71
|
+
// an unseen, already-gone name has no identity proving a selected alias.
|
|
72
|
+
if (!found)
|
|
73
|
+
continue;
|
|
74
|
+
for (const [relative, identity] of candidates) {
|
|
75
|
+
if (!relative || (path.dirname(relative) === "." ? "" : path.dirname(relative)) !== parent)
|
|
76
|
+
continue;
|
|
77
|
+
if (identity.dev !== found.identity.dev || identity.ino !== found.identity.ino)
|
|
78
|
+
continue;
|
|
79
|
+
for (const change of scopedChanges(scopes, { path: relative, type: "structural" }))
|
|
80
|
+
if (!add(change))
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
await assertRootDirectoryObservationGuard(root, guard);
|
|
84
|
+
}
|
|
85
|
+
await assertRootIdentityCurrent(root);
|
|
86
|
+
signal.throwIfAborted();
|
|
87
|
+
return [...result.values()];
|
|
88
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NativeWatchBatch } from "./watch-native.js";
|
|
2
|
+
import type { WatchSnapshot } from "./watch-scan.js";
|
|
3
|
+
import type { WatchChange, WatchScope } from "./watch-types.js";
|
|
4
|
+
export declare function excludedWatchPath(snapshot: WatchSnapshot | undefined, name: string): boolean;
|
|
5
|
+
export declare function scopedChanges(scopes: readonly WatchScope[], change: WatchChange): WatchChange[];
|
|
6
|
+
export declare function nativeChanges(scopes: readonly WatchScope[], snapshot: WatchSnapshot | undefined, batch: NativeWatchBatch, limit?: number, after?: WatchSnapshot): WatchChange[] | undefined;
|
|
7
|
+
export declare function changedEntries(before: WatchSnapshot | undefined, after: WatchSnapshot, limit: number): WatchChange[] | undefined;
|
|
8
|
+
/** Backend names never establish authority to publish a pathname. */
|
|
9
|
+
export declare function guardedHintChanges(scopes: readonly WatchScope[], before: WatchSnapshot | undefined, after: WatchSnapshot, hints: readonly WatchChange[] | undefined, observed: readonly WatchChange[] | undefined, limit: number): WatchChange[] | undefined;
|