@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.
Files changed (91) hide show
  1. package/dist/index.js +69 -111
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +35 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +1 -1
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -1
  6. package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
  7. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
  8. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  9. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
  10. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  11. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
  12. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
  13. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
  14. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +8 -5
  15. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +27 -50
  16. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.d.ts +3 -0
  17. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
  18. package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
  19. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
  20. package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
  21. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
  22. package/node_modules/@openclaw/fs-safe/dist/native-operations.d.ts +0 -1
  23. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
  24. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
  25. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +4 -1
  26. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
  27. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
  28. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
  29. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +34 -7
  30. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
  31. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
  32. package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
  33. package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
  34. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
  36. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
  37. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +15 -15
  38. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +1 -1
  39. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
  40. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
  41. package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
  42. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
  43. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
  44. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
  45. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
  46. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
  47. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
  48. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
  49. package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
  50. package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
  51. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
  52. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
  53. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
  54. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
  55. package/node_modules/@openclaw/fs-safe/dist/walk.js +2 -13
  56. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
  57. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
  58. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
  59. package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
  60. package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -1
  61. package/node_modules/@openclaw/fs-safe/docs/archive.md +3 -0
  62. package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
  63. package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
  64. package/node_modules/@openclaw/fs-safe/docs/copy.md +48 -0
  65. package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
  66. package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
  67. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
  68. package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
  69. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  70. package/node_modules/@openclaw/fs-safe/package.json +15 -15
  71. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  72. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  73. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  74. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  75. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  76. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  77. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  78. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  79. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  80. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  81. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  82. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  83. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  84. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  85. package/package.json +5 -5
  86. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
  87. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
  88. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +0 -4
  89. package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +0 -35
  90. package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
  91. 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 (!retained && !held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
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?: boolean): void;
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 = false) {
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", "temp workspace directory has an untrusted owner; use a private temp root owned by the effective user under a trusted directory hierarchy");
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", "temp workspace directory permissions are invalid");
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
- ? "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");
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", "temp workspace child must be a real directory");
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, receipt),
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?.stat;
153
+ return receipt;
155
154
  }
156
155
  finalizeAdmission(ownerUid, mode) {
157
156
  this.#assertModeLeaseReleased();
158
157
  this.discardInitialReceipt();
159
- if (this.#fd === undefined) {
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(this.#fd, this.#identity, this.#numericIdentity);
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
- if (this.#fd === undefined) {
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
- if (this.#fd === undefined) {
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
- if (this.#fd === undefined) {
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
- if (this.#fd === undefined) {
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 = buildEntry({ relativePath, fullPath, dirent, depth, kind });
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 = buildEntry({ relativePath, fullPath, dirent, depth, kind });
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 { WatchStreamPaths } from "./watch-stream.js";
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(paths: WatchStreamPaths): boolean;
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(paths) {
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;