@openclaw/openshell-sandbox 2026.10.2 → 2026.10.5-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +35 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +8 -5
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +27 -50
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +34 -7
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +15 -15
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/archive.md +3 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +48 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
- package/node_modules/@openclaw/fs-safe/package.json +15 -15
- 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/package.json +5 -5
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +0 -4
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +0 -35
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { hasErrorCode } from "./file-cleanup.js";
|
|
5
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
6
|
+
import { nativeOpenFlags } from "./native-operations.js";
|
|
7
|
+
import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
|
|
8
|
+
function mismatch(check) {
|
|
9
|
+
return new FsSafeError("path-mismatch", `created sidecar lock ${check} changed; replacement preserved`);
|
|
10
|
+
}
|
|
11
|
+
function assertOwnerRecord(fd, expected) {
|
|
12
|
+
if (fs.fstatSync(fd, { bigint: true }).size !== BigInt(expected.length))
|
|
13
|
+
throw mismatch("owner record");
|
|
14
|
+
const bytes = Buffer.alloc(expected.length);
|
|
15
|
+
let offset = 0;
|
|
16
|
+
while (offset < bytes.length) {
|
|
17
|
+
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
18
|
+
if (read === 0)
|
|
19
|
+
throw mismatch("owner record");
|
|
20
|
+
offset += read;
|
|
21
|
+
}
|
|
22
|
+
if (!bytes.equals(expected))
|
|
23
|
+
throw mismatch("owner record");
|
|
24
|
+
}
|
|
25
|
+
/** Borrowed creation authority: never reopens a path or closes the writer's fds. */
|
|
26
|
+
export function cleanupCreatedSidecar(lockPath, raw, fd, parent, identity) {
|
|
27
|
+
const name = path.basename(lockPath);
|
|
28
|
+
const windows = process.platform === "win32";
|
|
29
|
+
try {
|
|
30
|
+
if (windows) {
|
|
31
|
+
if (!parent.binding.rootRemovalStat || !parent.binding.rootRemovalUnlink) {
|
|
32
|
+
throw new FsSafeError("helper-unavailable", "creator cleanup is unavailable");
|
|
33
|
+
}
|
|
34
|
+
const entry = parent.binding.rootRemovalStat(parent.fd, name);
|
|
35
|
+
if (entry.directory || entry.symlink || !sameFileIdentityForCleanup(identity, entry))
|
|
36
|
+
throw mismatch("identity");
|
|
37
|
+
}
|
|
38
|
+
else if (!parent.binding.stagedFileMatches || !parent.binding.removeStagedFile) {
|
|
39
|
+
throw new FsSafeError("helper-unavailable", "creator cleanup is unavailable");
|
|
40
|
+
}
|
|
41
|
+
else if (!parent.binding.stagedFileMatches(parent.fd, name, fd)) {
|
|
42
|
+
throw mismatch("identity");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
47
|
+
return;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
51
|
+
if (!stat.isFile() || stat.nlink !== 1n || !sameFileIdentityForCleanup(identity, stat))
|
|
52
|
+
throw mismatch("identity");
|
|
53
|
+
assertOwnerRecord(fd, Buffer.from(raw));
|
|
54
|
+
if (windows) {
|
|
55
|
+
try {
|
|
56
|
+
parent.binding.rootRemovalUnlink(parent.fd, name, identity.dev, identity.ino, false);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (!hasErrorCode(error, "ENOENT"))
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
if (parent.binding.removeStagedFile(parent.fd, name, fd) === "preserved")
|
|
65
|
+
throw mismatch("identity");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Called only by the exclusive creator while its verified descriptor is alive. */
|
|
69
|
+
export function retainCreatedSidecar(lockPath, raw, createdFd, guard, nativeParent) {
|
|
70
|
+
const binding = nativeParent?.binding;
|
|
71
|
+
if (!binding)
|
|
72
|
+
return undefined;
|
|
73
|
+
const created = fs.fstatSync(createdFd, { bigint: true });
|
|
74
|
+
if (!created.isFile() || created.nlink !== 1n)
|
|
75
|
+
throw mismatch("identity");
|
|
76
|
+
const identity = Object.freeze({ dev: created.dev, ino: created.ino });
|
|
77
|
+
const name = path.basename(lockPath);
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
if (!binding.retainWindowsSidecar)
|
|
80
|
+
return undefined;
|
|
81
|
+
const native = binding.retainWindowsSidecar(nativeParent.fd, guard.realPath, name, BigInt(guard.stat.dev), BigInt(guard.stat.ino), created.dev, created.ino, BigInt(Buffer.byteLength(raw)), sha256Hex(raw), 1024 * 1024);
|
|
82
|
+
const admitted = native.admission;
|
|
83
|
+
if (admitted.status === "unsupported" && admitted.resources === "closed")
|
|
84
|
+
return undefined;
|
|
85
|
+
if (admitted.status !== "retained") {
|
|
86
|
+
throw new FsSafeError("helper-failed", "could not retain created sidecar lock", { details: { result: admitted } });
|
|
87
|
+
}
|
|
88
|
+
return { identity, settle(remove, reportMismatch = true) {
|
|
89
|
+
const result = native.settle(remove);
|
|
90
|
+
if (result.status === "preserved-mismatch") {
|
|
91
|
+
if (reportMismatch || result.resources !== "closed") {
|
|
92
|
+
throw mismatch(result.errors.some(error => /digest|size changed/u.test(error.message)) ? "owner record" : "identity");
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (result.errors.length || result.resources !== "closed" ||
|
|
97
|
+
(remove && result.disposition !== "accepted" && result.namespace !== "absent")) {
|
|
98
|
+
throw new FsSafeError("helper-failed", "retained sidecar lock settlement failed", { details: { result } });
|
|
99
|
+
}
|
|
100
|
+
} };
|
|
101
|
+
}
|
|
102
|
+
if (!binding.stagedFileMatches || !binding.removeStagedFile)
|
|
103
|
+
return undefined;
|
|
104
|
+
const closeFile = captureNativeFdClose(binding);
|
|
105
|
+
const parentFd = binding.openBeneath(nativeParent.fd, "", nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_DIRECTORY)).fd;
|
|
106
|
+
let fd;
|
|
107
|
+
try {
|
|
108
|
+
const parentStat = fs.fstatSync(parentFd, { bigint: true });
|
|
109
|
+
if (!parentStat.isDirectory() || !sameFileIdentityForCleanup(guard.stat, parentStat))
|
|
110
|
+
throw mismatch("identity");
|
|
111
|
+
fd = binding.openBeneath(parentFd, name, nativeOpenFlags(fs.constants.O_RDONLY | fs.constants.O_NONBLOCK)).fd;
|
|
112
|
+
const ownedFd = fd;
|
|
113
|
+
const assertIdentity = () => {
|
|
114
|
+
try {
|
|
115
|
+
if (!binding.stagedFileMatches(parentFd, name, ownedFd))
|
|
116
|
+
throw mismatch("identity");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (hasErrorCode(error, "ENOENT"))
|
|
120
|
+
return false;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const current = fs.fstatSync(ownedFd, { bigint: true });
|
|
124
|
+
if (!current.isFile() || current.nlink !== 1n || !sameFileIdentityForCleanup(created, current))
|
|
125
|
+
throw mismatch("identity");
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
const expected = Buffer.from(raw);
|
|
129
|
+
const assertOwner = () => assertOwnerRecord(ownedFd, expected);
|
|
130
|
+
if (!assertIdentity())
|
|
131
|
+
throw mismatch("identity");
|
|
132
|
+
assertOwner();
|
|
133
|
+
let settled = false;
|
|
134
|
+
return { identity, settle(remove, reportMismatch = true) {
|
|
135
|
+
if (settled)
|
|
136
|
+
return;
|
|
137
|
+
settled = true;
|
|
138
|
+
const errors = [];
|
|
139
|
+
try {
|
|
140
|
+
if (remove && assertIdentity()) {
|
|
141
|
+
assertOwner();
|
|
142
|
+
if (assertIdentity() && binding.removeStagedFile(parentFd, name, ownedFd) === "preserved")
|
|
143
|
+
throw mismatch("identity");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (reportMismatch || !(error instanceof FsSafeError && error.code === "path-mismatch"))
|
|
148
|
+
errors.push(error);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
closeFile(ownedFd);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
errors.push(error);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
closeFile(parentFd);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
errors.push(error);
|
|
161
|
+
}
|
|
162
|
+
if (errors.length === 1)
|
|
163
|
+
throw errors[0];
|
|
164
|
+
if (errors.length)
|
|
165
|
+
throw new AggregateError(errors, "sidecar lock cleanup and close failed");
|
|
166
|
+
} };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errors = [error];
|
|
170
|
+
try {
|
|
171
|
+
if (fd !== undefined)
|
|
172
|
+
closeFile(fd);
|
|
173
|
+
}
|
|
174
|
+
catch (closeError) {
|
|
175
|
+
errors.push(closeError);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
closeFile(parentFd);
|
|
179
|
+
}
|
|
180
|
+
catch (closeError) {
|
|
181
|
+
errors.push(closeError);
|
|
182
|
+
}
|
|
183
|
+
if (errors.length > 1)
|
|
184
|
+
throw new AggregateError(errors, "sidecar retention and close failed");
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -108,7 +108,10 @@ function releaseAllLocksSync(state, options) {
|
|
|
108
108
|
void held.handle.close().catch(() => undefined);
|
|
109
109
|
try {
|
|
110
110
|
const retained = options?.preserveRetained === true && held.retainOnExit;
|
|
111
|
-
if (
|
|
111
|
+
if (held.retainedSidecar) {
|
|
112
|
+
held.retainedSidecar.settle(!retained);
|
|
113
|
+
}
|
|
114
|
+
else if (!retained && !held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
112
115
|
fsSync.rmSync(held.lockPath, { force: true });
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -186,6 +189,17 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
186
189
|
}
|
|
187
190
|
held.releasePromise = (async () => {
|
|
188
191
|
await held.handle.close().catch(() => undefined);
|
|
192
|
+
if (held.retainedSidecar) {
|
|
193
|
+
try {
|
|
194
|
+
held.retainedSidecar.settle(true);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (state.held.get(normalizedTargetPath) === held)
|
|
198
|
+
state.held.delete(normalizedTargetPath);
|
|
199
|
+
stopSidecarLockMonitoring(held);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
189
203
|
await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
|
|
190
204
|
lockRoot: held.lockRoot,
|
|
191
205
|
parsePayload: held.parsePayload,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function tempDirectoryMode(mode: number | bigint): string;
|
|
2
|
+
export declare function privateTempDirectoryFix(uid: number | undefined): string;
|
|
3
|
+
type TempDirectoryStat = {
|
|
4
|
+
isDirectory(): boolean;
|
|
5
|
+
isSymbolicLink(): boolean;
|
|
6
|
+
uid?: number | bigint;
|
|
7
|
+
mode?: number | bigint;
|
|
8
|
+
};
|
|
9
|
+
export declare function secureTempDirectoryFailure(stat: TempDirectoryStat, uid: number | undefined, windows: boolean): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function tempDirectoryMode(mode) {
|
|
2
|
+
return (typeof mode === "bigint" ? mode & 4095n : mode & 0o7777).toString(8).padStart(4, "0");
|
|
3
|
+
}
|
|
4
|
+
export function privateTempDirectoryFix(uid) {
|
|
5
|
+
return `use a real private directory${uid === undefined ? "" : ` owned by uid ${uid}`} with mode 0700 under trusted ancestors (or use resolveSecureTempRoot()); do not chmod a shared temp directory to 0700`;
|
|
6
|
+
}
|
|
7
|
+
// Called only after failed admission; diagnostics never authorize repair.
|
|
8
|
+
export function secureTempDirectoryFailure(stat, uid, windows) {
|
|
9
|
+
if (stat.isSymbolicLink())
|
|
10
|
+
return "is a symbolic link; choose a real directory, not a symlink";
|
|
11
|
+
if (!stat.isDirectory())
|
|
12
|
+
return "is not a directory; choose a real directory";
|
|
13
|
+
if (!windows && uid !== undefined) {
|
|
14
|
+
if (stat.uid !== undefined && stat.uid !== uid && stat.uid !== BigInt(uid)) {
|
|
15
|
+
return `has owner uid ${stat.uid}, expected uid ${uid}; ${privateTempDirectoryFix(uid)}`;
|
|
16
|
+
}
|
|
17
|
+
const mode = stat.mode;
|
|
18
|
+
if (mode !== undefined) {
|
|
19
|
+
if ((typeof mode === "number" && (!Number.isSafeInteger(mode) || mode < 0)) ||
|
|
20
|
+
(typeof mode === "bigint" && mode < 0n)) {
|
|
21
|
+
return `has invalid mode bits; ${privateTempDirectoryFix(uid)}`;
|
|
22
|
+
}
|
|
23
|
+
if (typeof mode === "bigint" ? (mode & 18n) !== 0n : (mode & 0o022) !== 0) {
|
|
24
|
+
return `has group/world-writable mode ${tempDirectoryMode(mode)} (a sticky bit does not make a private root safe); ${privateTempDirectoryFix(uid)}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return `directory identity, permissions, or write/search access could not be verified; ${privateTempDirectoryFix(uid)}`;
|
|
29
|
+
}
|
|
@@ -143,7 +143,7 @@ export async function createOwnedTempFile(params) {
|
|
|
143
143
|
capability.assertCurrent();
|
|
144
144
|
const initial = inspectDirectoryIdentitySync(dir);
|
|
145
145
|
identity = Object.freeze({ dev: initial.dev, ino: initial.ino });
|
|
146
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(initial, admission.ownerUid, 0o700);
|
|
146
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(initial, admission.ownerUid, 0o700, dir);
|
|
147
147
|
retainedChild = TempWorkspaceRetainedChild.retain(dir, identity);
|
|
148
148
|
if (needsModeInitialization) {
|
|
149
149
|
await retainedChild.initializeMode(0o700, admission.ownerUid, admission.assertCurrent);
|
|
@@ -46,7 +46,7 @@ function snapshotFromExactObservation(dir, uid, realPath, stat, privateDirectory
|
|
|
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, privateDirectory);
|
|
49
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory, dir);
|
|
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.
|
|
@@ -106,7 +106,7 @@ function inspectSnapshotIdentity(entry) {
|
|
|
106
106
|
}
|
|
107
107
|
function assertSnapshot(entry, uid) {
|
|
108
108
|
const stat = inspectSnapshotIdentity(entry);
|
|
109
|
-
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory);
|
|
109
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory, entry.dir);
|
|
110
110
|
assertCanonicalRoot(entry);
|
|
111
111
|
}
|
|
112
112
|
function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
@@ -115,7 +115,7 @@ function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
|
115
115
|
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
116
116
|
}
|
|
117
117
|
const current = inspectSnapshotIdentity(parent);
|
|
118
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory);
|
|
118
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory, parent.dir);
|
|
119
119
|
let realPath;
|
|
120
120
|
try {
|
|
121
121
|
realPath = canonicalTempWorkspacePath(dir);
|
|
@@ -136,7 +136,7 @@ function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
|
136
136
|
function assertChain(chain, uid) {
|
|
137
137
|
for (const entry of chain) {
|
|
138
138
|
const current = inspectSnapshotIdentity(entry);
|
|
139
|
-
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory);
|
|
139
|
+
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory, entry.dir);
|
|
140
140
|
}
|
|
141
141
|
const last = chain[chain.length - 1];
|
|
142
142
|
assertCanonicalRoot(last);
|
|
@@ -159,7 +159,7 @@ function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
|
|
|
159
159
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
160
160
|
throw new FsSafeError("not-file", "temp workspace cleanup parent must be a real directory");
|
|
161
161
|
}
|
|
162
|
-
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory);
|
|
162
|
+
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory, entry.dir);
|
|
163
163
|
}
|
|
164
164
|
function discoverMissingTempWorkspaceAncestor(ancestor, missing, missingError) {
|
|
165
165
|
// The caller already classified the first ENOENT. Keep this handoff out of
|
|
@@ -290,7 +290,7 @@ function canonicalRootAdmission(discovery, ownerUid) {
|
|
|
290
290
|
return snapshot(dir, ownerUid, dir, index === ancestry.length - 1 && discovery.privateDirectory).entry;
|
|
291
291
|
}
|
|
292
292
|
const current = inspectSnapshotIdentity(discovery);
|
|
293
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory);
|
|
293
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory, discovery.dir);
|
|
294
294
|
return discovery;
|
|
295
295
|
});
|
|
296
296
|
const current = candidate[candidate.length - 1];
|
|
@@ -14,10 +14,10 @@ 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" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory
|
|
18
|
-
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined): void;
|
|
19
|
-
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number): boolean;
|
|
17
|
+
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory: boolean, dir: string): void;
|
|
18
|
+
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined, dir: string): void;
|
|
19
|
+
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number, dir: string): boolean;
|
|
20
20
|
export declare function childHasRequestedMode(stat: BigIntStats | Stats, mode: number): boolean;
|
|
21
21
|
export declare function admitTempWorkspaceChild(dir: string, expected: BigIntStats, parent: TempWorkspaceRootAdmission, mode: number): Promise<void> | undefined;
|
|
22
22
|
export declare function admitTempWorkspaceChildSync(dir: string, expected: BigIntStats, parent: TempWorkspaceRootAdmission, mode: number): void;
|
|
23
|
-
export declare function validateAdmittedTempWorkspaceChild(current: TempWorkspaceIdentityStat, ownerUid: number | undefined, mode: number): void;
|
|
23
|
+
export declare function validateAdmittedTempWorkspaceChild(current: TempWorkspaceIdentityStat, ownerUid: number | undefined, mode: number, dir: string): void;
|
|
@@ -4,6 +4,7 @@ import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./director
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
6
|
import { classifyTempWorkspaceOwner, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
|
|
7
|
+
import { privateTempDirectoryFix, tempDirectoryMode } from "./temp-directory-diagnostics.js";
|
|
7
8
|
export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
|
|
8
9
|
export function projectTempWorkspaceNumericIdentity(identity) {
|
|
9
10
|
const dev = Number(identity.dev);
|
|
@@ -43,12 +44,12 @@ export function validateTempWorkspaceDirMode(mode) {
|
|
|
43
44
|
throw new FsSafeError("insecure-permissions", "temp workspace must not be group/world writable");
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory
|
|
47
|
+
export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory, dir) {
|
|
47
48
|
if (uid === undefined)
|
|
48
49
|
return;
|
|
49
50
|
const ownership = classifyTempWorkspaceOwner(stat, uid);
|
|
50
51
|
if (ownership === "foreign" || (privateDirectory && ownership !== "user")) {
|
|
51
|
-
throw new FsSafeError("not-owned",
|
|
52
|
+
throw new FsSafeError("not-owned", `temp workspace directory ${JSON.stringify(dir)} has an untrusted owner uid ${stat.uid}; expected uid ${uid}${privateDirectory || uid === 0 ? "" : " or root uid 0"}; ${privateTempDirectoryFix(uid)}`);
|
|
52
53
|
}
|
|
53
54
|
// Unmapped host owners are unverifiable; trust the host directory hierarchy.
|
|
54
55
|
// Admit sticky ancestors so PrivateUsers keeps host /tmp and PrivateTmp usable.
|
|
@@ -60,18 +61,18 @@ export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory
|
|
|
60
61
|
? (stat.mode & 512n) !== 0n
|
|
61
62
|
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o1000) !== 0;
|
|
62
63
|
if (typeof stat.mode !== "bigint" && (!Number.isSafeInteger(stat.mode) || stat.mode < 0)) {
|
|
63
|
-
throw new FsSafeError("insecure-permissions",
|
|
64
|
+
throw new FsSafeError("insecure-permissions", `temp workspace directory ${JSON.stringify(dir)} permissions are invalid; ${privateTempDirectoryFix(uid)}`);
|
|
64
65
|
}
|
|
65
66
|
if (writable && (privateDirectory || !sticky)) {
|
|
66
67
|
throw new FsSafeError("insecure-permissions", privateDirectory
|
|
67
|
-
?
|
|
68
|
-
:
|
|
68
|
+
? `temp workspace directory ${JSON.stringify(dir)} has group/world-writable mode ${tempDirectoryMode(stat.mode)}; root and child must not be group/world writable, even with the sticky bit; ${privateTempDirectoryFix(uid)}`
|
|
69
|
+
: `temp workspace ancestor ${JSON.stringify(dir)} has group/world-writable mode ${tempDirectoryMode(stat.mode)} without sticky protection (sticky bit missing); have its administrator restore safe permissions (1777 for a shared system temp directory), or move the private root under trusted ancestors`);
|
|
69
70
|
}
|
|
70
71
|
if (ownership === "unmapped")
|
|
71
72
|
warnUnmappedTempWorkspaceAncestor();
|
|
72
73
|
}
|
|
73
74
|
const WINDOWS = process.platform === "win32";
|
|
74
|
-
export function assertTempWorkspaceChildState(stat, ownerUid) {
|
|
75
|
+
export function assertTempWorkspaceChildState(stat, ownerUid, dir) {
|
|
75
76
|
const exactIdentity = typeof stat.dev === "bigint" && typeof stat.ino === "bigint" &&
|
|
76
77
|
(!WINDOWS || (stat.dev !== 0n && stat.ino !== 0n));
|
|
77
78
|
const safeNumericIdentity = TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY &&
|
|
@@ -81,12 +82,12 @@ export function assertTempWorkspaceChildState(stat, ownerUid) {
|
|
|
81
82
|
throw new FsSafeError("path-mismatch", "temp workspace child identity could not be verified");
|
|
82
83
|
}
|
|
83
84
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
84
|
-
throw new FsSafeError("not-file",
|
|
85
|
+
throw new FsSafeError("not-file", `temp workspace child ${JSON.stringify(dir)} ${stat.isSymbolicLink() ? "is a symbolic link" : "is not a directory"}; ${privateTempDirectoryFix(ownerUid)}`);
|
|
85
86
|
}
|
|
86
|
-
assertTrustedTempWorkspaceDirectory(stat, ownerUid, true);
|
|
87
|
+
assertTrustedTempWorkspaceDirectory(stat, ownerUid, true, dir);
|
|
87
88
|
}
|
|
88
|
-
export function validateInitialTempWorkspaceChild(stat, ownerUid, mode) {
|
|
89
|
-
assertTempWorkspaceChildState(stat, ownerUid);
|
|
89
|
+
export function validateInitialTempWorkspaceChild(stat, ownerUid, mode, dir) {
|
|
90
|
+
assertTempWorkspaceChildState(stat, ownerUid, dir);
|
|
90
91
|
return !childHasRequestedMode(stat, mode);
|
|
91
92
|
}
|
|
92
93
|
export function childHasRequestedMode(stat, mode) {
|
|
@@ -105,7 +106,7 @@ async function initializeTempWorkspaceChildMode(dir, expected, parent, mode) {
|
|
|
105
106
|
try {
|
|
106
107
|
await owner.apply(mode, { check: parent.assertCurrent });
|
|
107
108
|
const current = inspectDirectoryIdentitySync(dir, expected);
|
|
108
|
-
validateAdmittedTempWorkspaceChild(current, parent.ownerUid, mode);
|
|
109
|
+
validateAdmittedTempWorkspaceChild(current, parent.ownerUid, mode, dir);
|
|
109
110
|
parent.assertCurrent();
|
|
110
111
|
}
|
|
111
112
|
finally {
|
|
@@ -113,12 +114,12 @@ async function initializeTempWorkspaceChildMode(dir, expected, parent, mode) {
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
export function admitTempWorkspaceChild(dir, expected, parent, mode) {
|
|
116
|
-
if (!validateInitialTempWorkspaceChild(expected, parent.ownerUid, mode))
|
|
117
|
+
if (!validateInitialTempWorkspaceChild(expected, parent.ownerUid, mode, dir))
|
|
117
118
|
return undefined;
|
|
118
119
|
return initializeTempWorkspaceChildMode(dir, expected, parent, mode);
|
|
119
120
|
}
|
|
120
121
|
export function admitTempWorkspaceChildSync(dir, expected, parent, mode) {
|
|
121
|
-
if (!validateInitialTempWorkspaceChild(expected, parent.ownerUid, mode))
|
|
122
|
+
if (!validateInitialTempWorkspaceChild(expected, parent.ownerUid, mode, dir))
|
|
122
123
|
return;
|
|
123
124
|
parent.assertCurrent();
|
|
124
125
|
const owner = pinNodeDirectoryForModeSync(dir, {
|
|
@@ -128,15 +129,15 @@ export function admitTempWorkspaceChildSync(dir, expected, parent, mode) {
|
|
|
128
129
|
try {
|
|
129
130
|
owner.apply(mode, parent.assertCurrent);
|
|
130
131
|
const current = inspectDirectoryIdentitySync(dir, expected);
|
|
131
|
-
validateAdmittedTempWorkspaceChild(current, parent.ownerUid, mode);
|
|
132
|
+
validateAdmittedTempWorkspaceChild(current, parent.ownerUid, mode, dir);
|
|
132
133
|
parent.assertCurrent();
|
|
133
134
|
}
|
|
134
135
|
finally {
|
|
135
136
|
owner.close();
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
|
-
export function validateAdmittedTempWorkspaceChild(current, ownerUid, mode) {
|
|
139
|
-
assertTempWorkspaceChildState(current, ownerUid);
|
|
139
|
+
export function validateAdmittedTempWorkspaceChild(current, ownerUid, mode, dir) {
|
|
140
|
+
assertTempWorkspaceChildState(current, ownerUid, dir);
|
|
140
141
|
if (!childHasRequestedMode(current, mode)) {
|
|
141
142
|
throw new FsSafeError("path-mismatch", "temp workspace final mode could not be verified");
|
|
142
143
|
}
|
|
@@ -133,9 +133,8 @@ export class TempWorkspaceRetainedChild {
|
|
|
133
133
|
// the synchronous caller validates it and decides whether to correct.
|
|
134
134
|
const stat = fsSync.fstatSync(descriptor.fd, { bigint: true });
|
|
135
135
|
Object.freeze(stat);
|
|
136
|
-
const receipt = Object.freeze({ stat });
|
|
137
136
|
return {
|
|
138
|
-
retained: new TempWorkspaceRetainedChild(dir, stat, descriptor,
|
|
137
|
+
retained: new TempWorkspaceRetainedChild(dir, stat, descriptor, stat),
|
|
139
138
|
stat,
|
|
140
139
|
};
|
|
141
140
|
}
|
|
@@ -151,23 +150,27 @@ export class TempWorkspaceRetainedChild {
|
|
|
151
150
|
// Clear first so validation failures, procfs checks, and later admission
|
|
152
151
|
// can never reuse the creation observation.
|
|
153
152
|
this.#initialReceipt = undefined;
|
|
154
|
-
return receipt
|
|
153
|
+
return receipt;
|
|
155
154
|
}
|
|
156
155
|
finalizeAdmission(ownerUid, mode) {
|
|
157
156
|
this.#assertModeLeaseReleased();
|
|
158
157
|
this.discardInitialReceipt();
|
|
159
|
-
|
|
160
|
-
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
161
|
-
}
|
|
158
|
+
const fd = this.#requireDescriptor();
|
|
162
159
|
// A failed revalidation must never leave an earlier receipt transferable.
|
|
163
160
|
this.#transferAuthorized = false;
|
|
164
|
-
const current = inspectTempWorkspaceDescriptorIdentitySync(
|
|
165
|
-
validateAdmittedTempWorkspaceChild(current, ownerUid, mode);
|
|
161
|
+
const current = inspectTempWorkspaceDescriptorIdentitySync(fd, this.#identity, this.#numericIdentity);
|
|
162
|
+
validateAdmittedTempWorkspaceChild(current, ownerUid, mode, this.#dir);
|
|
166
163
|
const named = inspectTempWorkspaceDirectoryIdentitySync(this.#dir, this.#identity, this.#numericIdentity);
|
|
167
|
-
validateAdmittedTempWorkspaceChild(named, ownerUid, mode);
|
|
164
|
+
validateAdmittedTempWorkspaceChild(named, ownerUid, mode, this.#dir);
|
|
168
165
|
this.#transferAuthorized = true;
|
|
169
166
|
return named;
|
|
170
167
|
}
|
|
168
|
+
#requireDescriptor() {
|
|
169
|
+
if (this.#fd === undefined) {
|
|
170
|
+
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
171
|
+
}
|
|
172
|
+
return this.#fd;
|
|
173
|
+
}
|
|
171
174
|
#assertModeLeaseReleased() {
|
|
172
175
|
if (this.#modeLease) {
|
|
173
176
|
throw new FsSafeError("path-mismatch", "temp workspace child mode operation is active");
|
|
@@ -175,9 +178,9 @@ export class TempWorkspaceRetainedChild {
|
|
|
175
178
|
}
|
|
176
179
|
#inspectModeTarget(fd, ownerUid, initial) {
|
|
177
180
|
const descriptor = initial ?? inspectTempWorkspaceDescriptorIdentitySync(fd, this.#identity, this.#numericIdentity);
|
|
178
|
-
assertTempWorkspaceChildState(descriptor, ownerUid);
|
|
181
|
+
assertTempWorkspaceChildState(descriptor, ownerUid, this.#dir);
|
|
179
182
|
const named = inspectTempWorkspaceDirectoryIdentitySync(this.#dir, this.#identity, this.#numericIdentity);
|
|
180
|
-
assertTempWorkspaceChildState(named, ownerUid);
|
|
183
|
+
assertTempWorkspaceChildState(named, ownerUid, this.#dir);
|
|
181
184
|
return descriptor;
|
|
182
185
|
}
|
|
183
186
|
async #assertProcAuthority(fd, ownerUid) {
|
|
@@ -196,16 +199,13 @@ export class TempWorkspaceRetainedChild {
|
|
|
196
199
|
const opened = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }), this.#identity);
|
|
197
200
|
const followed = inspectFileIdentitySync(() => fsSync.statSync(procPath, { bigint: true }), this.#identity);
|
|
198
201
|
assertOwnedDirectory(opened, followed);
|
|
199
|
-
assertTempWorkspaceChildState(opened, ownerUid);
|
|
200
|
-
assertTempWorkspaceChildState(followed, ownerUid);
|
|
202
|
+
assertTempWorkspaceChildState(opened, ownerUid, this.#dir);
|
|
203
|
+
assertTempWorkspaceChildState(followed, ownerUid, this.#dir);
|
|
201
204
|
}
|
|
202
205
|
async initializeMode(mode, ownerUid, assertParent) {
|
|
203
206
|
this.#assertModeLeaseReleased();
|
|
204
207
|
this.discardInitialReceipt();
|
|
205
|
-
|
|
206
|
-
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
207
|
-
}
|
|
208
|
-
const fd = this.#fd;
|
|
208
|
+
const fd = this.#requireDescriptor();
|
|
209
209
|
this.#transferAuthorized = false;
|
|
210
210
|
this.#modeLease = true;
|
|
211
211
|
try {
|
|
@@ -238,10 +238,7 @@ export class TempWorkspaceRetainedChild {
|
|
|
238
238
|
}
|
|
239
239
|
initializeModeSync(mode, ownerUid, assertParent) {
|
|
240
240
|
this.#assertModeLeaseReleased();
|
|
241
|
-
|
|
242
|
-
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
243
|
-
}
|
|
244
|
-
const fd = this.#fd;
|
|
241
|
+
const fd = this.#requireDescriptor();
|
|
245
242
|
this.#transferAuthorized = false;
|
|
246
243
|
this.#modeLease = true;
|
|
247
244
|
try {
|
|
@@ -270,9 +267,7 @@ export class TempWorkspaceRetainedChild {
|
|
|
270
267
|
this.discardInitialReceipt();
|
|
271
268
|
if (this.canEnumerate)
|
|
272
269
|
return true;
|
|
273
|
-
|
|
274
|
-
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
275
|
-
}
|
|
270
|
+
this.#requireDescriptor();
|
|
276
271
|
// Opening or validating a replacement crosses a new pathname boundary.
|
|
277
272
|
// Even failure must revoke any earlier final-admission receipt.
|
|
278
273
|
this.#transferAuthorized = false;
|
|
@@ -312,13 +307,10 @@ export class TempWorkspaceRetainedChild {
|
|
|
312
307
|
transfer(retainDescriptor) {
|
|
313
308
|
this.#assertModeLeaseReleased();
|
|
314
309
|
this.discardInitialReceipt();
|
|
315
|
-
|
|
316
|
-
throw new FsSafeError("path-mismatch", "temp workspace child descriptor is unavailable");
|
|
317
|
-
}
|
|
310
|
+
const fd = this.#requireDescriptor();
|
|
318
311
|
if (!this.#transferAuthorized) {
|
|
319
312
|
throw new FsSafeError("path-mismatch", "temp workspace child has not completed final admission");
|
|
320
313
|
}
|
|
321
|
-
const fd = this.#fd;
|
|
322
314
|
this.#fd = undefined;
|
|
323
315
|
if (retainDescriptor && !this.canEnumerate) {
|
|
324
316
|
closeAfterAdmissionFailure(fd, new FsSafeError("helper-unavailable", "temp workspace cleanup requires a readable child descriptor"), "temp workspace child descriptor rejection and close failed");
|
|
@@ -32,17 +32,6 @@ function kindForDirent(dirent) {
|
|
|
32
32
|
function shouldStop(result, options) {
|
|
33
33
|
return options.maxEntries !== undefined && result.scannedEntryCount >= Math.max(0, options.maxEntries);
|
|
34
34
|
}
|
|
35
|
-
function buildEntry(params) {
|
|
36
|
-
const fullPath = params.fullPath;
|
|
37
|
-
return {
|
|
38
|
-
name: params.dirent.name,
|
|
39
|
-
path: fullPath,
|
|
40
|
-
relativePath: params.relativePath,
|
|
41
|
-
depth: params.depth,
|
|
42
|
-
kind: params.kind,
|
|
43
|
-
dirent: params.dirent,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
35
|
function recordFailedDir(result, root, dir, depth, error) {
|
|
47
36
|
const relativePath = path.relative(root, dir);
|
|
48
37
|
result.failedDirs.push({
|
|
@@ -138,7 +127,7 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
138
127
|
if (!kind)
|
|
139
128
|
continue;
|
|
140
129
|
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
141
|
-
const entry =
|
|
130
|
+
const entry = { name: dirent.name, path: fullPath, relativePath, depth, kind, dirent };
|
|
142
131
|
if (options.include?.(entry) ?? true) {
|
|
143
132
|
result.entries.push(entry);
|
|
144
133
|
}
|
|
@@ -229,7 +218,7 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
229
218
|
if (!kind)
|
|
230
219
|
continue;
|
|
231
220
|
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
232
|
-
const entry =
|
|
221
|
+
const entry = { name: dirent.name, path: fullPath, relativePath, depth, kind, dirent };
|
|
233
222
|
const include = options.include;
|
|
234
223
|
const included = include == null ? true : Reflect.apply(include, options, [entry]);
|
|
235
224
|
if ((isObjectResult(included) ? await included : included) ?? true) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type NativeBinding } from "./native.js";
|
|
2
2
|
import type { RootContext } from "./root-context.js";
|
|
3
3
|
import type { DirectoryIdentity, WatchSnapshot } from "./watch-scan.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { WatchScope } from "./watch-types.js";
|
|
5
5
|
export type NativeWatchHint = {
|
|
6
6
|
directory: string;
|
|
7
7
|
name: string;
|
|
@@ -50,6 +50,6 @@ export declare class NativeWatchBackend {
|
|
|
50
50
|
add(name: string, identity: DirectoryIdentity): void;
|
|
51
51
|
testEvent(path: string, flags: number): void;
|
|
52
52
|
entries(snapshot: WatchSnapshot): boolean;
|
|
53
|
-
configure(
|
|
53
|
+
configure(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): boolean;
|
|
54
54
|
close(): void;
|
|
55
55
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
3
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
4
|
+
import { watchStreamPaths } from "./watch-stream.js";
|
|
4
5
|
export function watchBinding(mode) {
|
|
5
6
|
if (mode === "poll")
|
|
6
7
|
return;
|
|
@@ -61,9 +62,10 @@ export class NativeWatchBackend {
|
|
|
61
62
|
throw watchError(cause);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
configure(
|
|
65
|
+
configure(snapshot, scopes) {
|
|
65
66
|
if (process.platform !== "darwin")
|
|
66
67
|
return false;
|
|
68
|
+
const paths = watchStreamPaths(snapshot, scopes);
|
|
67
69
|
const key = JSON.stringify(paths);
|
|
68
70
|
if (this.streamPaths === key)
|
|
69
71
|
return false;
|