@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
package/dist/index.js
CHANGED
|
@@ -189,29 +189,24 @@ var OpenShellFsBridge = class {
|
|
|
189
189
|
return entries;
|
|
190
190
|
}
|
|
191
191
|
async writeFile(params) {
|
|
192
|
-
|
|
193
|
-
const hostPath = target.hostPath;
|
|
194
|
-
this.ensureWritable(target, "write files");
|
|
195
|
-
const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
|
|
196
|
-
await (await root(target.mountHostRoot)).write(relativeToRoot(target, hostPath), buffer, {
|
|
197
|
-
mkdir: params.mkdir,
|
|
198
|
-
mutationSymlinks: "reject"
|
|
199
|
-
});
|
|
200
|
-
await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
|
|
192
|
+
await this.writeLocalFile(params, "write");
|
|
201
193
|
}
|
|
202
194
|
async createFileExclusive(params) {
|
|
195
|
+
return this.writeLocalFile(params, "create");
|
|
196
|
+
}
|
|
197
|
+
async writeLocalFile(params, method) {
|
|
203
198
|
const target = this.resolveTarget(params);
|
|
204
199
|
const hostPath = target.hostPath;
|
|
205
|
-
this.ensureWritable(target, "create files");
|
|
200
|
+
this.ensureWritable(target, method === "create" ? "create files" : "write files");
|
|
206
201
|
const buffer = Buffer.isBuffer(params.data) ? params.data : Buffer.from(params.data, params.encoding ?? "utf8");
|
|
207
202
|
const root$2 = await root(target.mountHostRoot);
|
|
208
203
|
try {
|
|
209
|
-
await root$2
|
|
210
|
-
mkdir: params.mkdir !== false,
|
|
204
|
+
await root$2[method](relativeToRoot(target, hostPath), buffer, {
|
|
205
|
+
mkdir: method === "create" ? params.mkdir !== false : params.mkdir,
|
|
211
206
|
mutationSymlinks: "reject"
|
|
212
207
|
});
|
|
213
208
|
} catch (error) {
|
|
214
|
-
if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
|
|
209
|
+
if (method === "create" && error instanceof FsSafeError && error.code === "already-exists") return "exists";
|
|
215
210
|
throw error;
|
|
216
211
|
}
|
|
217
212
|
await this.backend.syncLocalPathToRemote(hostPath, target.containerPath);
|
|
@@ -366,25 +361,25 @@ var OpenShellFsBridge = class {
|
|
|
366
361
|
}
|
|
367
362
|
const cwd = params.cwd ? path.resolve(params.cwd) : workspaceRoot;
|
|
368
363
|
const hostPath = path.isAbsolute(input) ? path.resolve(input) : path.resolve(cwd, input);
|
|
369
|
-
for (const mount of
|
|
364
|
+
for (const mount of [
|
|
365
|
+
...readOnlyMounts,
|
|
366
|
+
{
|
|
367
|
+
hostPath: workspaceRoot,
|
|
368
|
+
containerPath: workspaceContainerRoot
|
|
369
|
+
},
|
|
370
|
+
...hasAgentMount ? [{
|
|
371
|
+
hostPath: agentRoot,
|
|
372
|
+
containerPath: agentContainerRoot
|
|
373
|
+
}] : []
|
|
374
|
+
]) if (isPathInside(mount.hostPath, hostPath)) {
|
|
370
375
|
const relative = path.relative(mount.hostPath, hostPath).split(path.sep).join(path.posix.sep);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const relative = path.relative(workspaceRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
375
|
-
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(workspaceContainerRoot, relative)), input);
|
|
376
|
-
}
|
|
377
|
-
if (hasAgentMount && isPathInside(agentRoot, hostPath)) {
|
|
378
|
-
const relative = path.relative(agentRoot, hostPath).split(path.sep).join(path.posix.sep);
|
|
379
|
-
return expectResolvedContainerTarget(resolveContainerTarget(path.posix.join(agentContainerRoot, relative)), input);
|
|
376
|
+
const target = resolveContainerTarget(path.posix.join(mount.containerPath, relative));
|
|
377
|
+
if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
378
|
+
return target;
|
|
380
379
|
}
|
|
381
380
|
throw new Error(`Path escapes sandbox root (${workspaceRoot}): ${params.filePath}`);
|
|
382
381
|
}
|
|
383
382
|
};
|
|
384
|
-
function expectResolvedContainerTarget(target, input) {
|
|
385
|
-
if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
386
|
-
return target;
|
|
387
|
-
}
|
|
388
383
|
async function removeLocalRootPath(params) {
|
|
389
384
|
const root$4 = await root(params.target.mountHostRoot);
|
|
390
385
|
const relativePath = relativeToRoot(params.target, params.target.hostPath);
|
|
@@ -529,16 +524,6 @@ async function stageDirectoryContents(params) {
|
|
|
529
524
|
const openShellWorkspaceOperations = new KeyedAsyncQueue();
|
|
530
525
|
let openShellDetachedCreateSupport;
|
|
531
526
|
const MATERIALIZED_SKILLS_REMOTE_PARTS = [".openclaw", "sandbox-skills"];
|
|
532
|
-
function buildOpenShellDirectoryUploadArgs(params) {
|
|
533
|
-
return [
|
|
534
|
-
"sandbox",
|
|
535
|
-
"upload",
|
|
536
|
-
"--no-git-ignore",
|
|
537
|
-
params.sandboxName,
|
|
538
|
-
params.localPath,
|
|
539
|
-
`${normalizeRemotePath(params.remotePath)}/`
|
|
540
|
-
];
|
|
541
|
-
}
|
|
542
527
|
const REMOTE_MANAGED_ROOTS_EMPTY_SCRIPT = "for root in \"$@\"; do if [ -d \"$root\" ] && [ -n \"$(ls -A \"$root\")\" ]; then printf \"1\\n\"; exit 0; fi; done; printf \"0\\n\"";
|
|
543
528
|
const OPEN_SHELL_GUEST_MUTATION_PYTHON = `
|
|
544
529
|
import os, sys
|
|
@@ -973,19 +958,27 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
973
958
|
target.root,
|
|
974
959
|
path.posix.dirname(target.relativePath) === "." ? "" : path.posix.dirname(target.relativePath)
|
|
975
960
|
] });
|
|
961
|
+
await this.uploadFileToRemote(localPath, remotePath);
|
|
962
|
+
}
|
|
963
|
+
async uploadFileToRemote(localPath, remotePath) {
|
|
964
|
+
await this.runCli([
|
|
965
|
+
"sandbox",
|
|
966
|
+
"upload",
|
|
967
|
+
"--no-git-ignore",
|
|
968
|
+
this.params.execContext.sandboxName,
|
|
969
|
+
localPath,
|
|
970
|
+
remotePath
|
|
971
|
+
], "openshell sandbox upload failed");
|
|
972
|
+
}
|
|
973
|
+
async runCli(args, failureMessage, timeoutMs) {
|
|
976
974
|
const result = await runOpenShellCli({
|
|
977
975
|
context: this.params.execContext,
|
|
978
|
-
args
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
"--no-git-ignore",
|
|
982
|
-
this.params.execContext.sandboxName,
|
|
983
|
-
localPath,
|
|
984
|
-
remotePath
|
|
985
|
-
],
|
|
986
|
-
cwd: this.params.createParams.workspaceDir
|
|
976
|
+
args,
|
|
977
|
+
cwd: this.params.createParams.workspaceDir,
|
|
978
|
+
...timeoutMs === void 0 ? {} : { timeoutMs }
|
|
987
979
|
});
|
|
988
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() ||
|
|
980
|
+
if (failureMessage && result.code !== 0) throw new Error(result.stderr.trim() || failureMessage);
|
|
981
|
+
return result;
|
|
989
982
|
}
|
|
990
983
|
async runPinnedRemotePathMutation(params) {
|
|
991
984
|
return await this.runRemoteShellScript({
|
|
@@ -1034,15 +1027,11 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1034
1027
|
}
|
|
1035
1028
|
}
|
|
1036
1029
|
async ensureSandboxExistsInner() {
|
|
1037
|
-
const getResult = await
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
this.params.execContext.sandboxName
|
|
1043
|
-
],
|
|
1044
|
-
cwd: this.params.createParams.workspaceDir
|
|
1045
|
-
});
|
|
1030
|
+
const getResult = await this.runCli([
|
|
1031
|
+
"sandbox",
|
|
1032
|
+
"get",
|
|
1033
|
+
this.params.execContext.sandboxName
|
|
1034
|
+
]);
|
|
1046
1035
|
if (getResult.code === 0) {
|
|
1047
1036
|
if (this.params.legacyRuntimeAdopted) {
|
|
1048
1037
|
const phase = await this.resolveLegacyRuntimePhase();
|
|
@@ -1073,13 +1062,7 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1073
1062
|
"infinity"
|
|
1074
1063
|
] : ["--", "true"]
|
|
1075
1064
|
];
|
|
1076
|
-
|
|
1077
|
-
context: this.params.execContext,
|
|
1078
|
-
args: createArgs,
|
|
1079
|
-
cwd: this.params.createParams.workspaceDir,
|
|
1080
|
-
timeoutMs: Math.max(this.params.execContext.config.timeoutMs, 3e5)
|
|
1081
|
-
});
|
|
1082
|
-
if (createResult.code !== 0) throw new Error(createResult.stderr.trim() || "openshell sandbox create failed");
|
|
1065
|
+
await this.runCli(createArgs, "openshell sandbox create failed", Math.max(this.params.execContext.config.timeoutMs, 3e5));
|
|
1083
1066
|
this.remoteSeedPending = true;
|
|
1084
1067
|
}
|
|
1085
1068
|
async supportsDetachedSandboxCreation() {
|
|
@@ -1093,16 +1076,11 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1093
1076
|
let support = openShellDetachedCreateSupport?.key === cliIdentity ? openShellDetachedCreateSupport.promise : void 0;
|
|
1094
1077
|
if (!support) {
|
|
1095
1078
|
support = (async () => {
|
|
1096
|
-
const result = await
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
"--help"
|
|
1102
|
-
],
|
|
1103
|
-
cwd: this.params.createParams.workspaceDir
|
|
1104
|
-
});
|
|
1105
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox create capability check failed");
|
|
1079
|
+
const result = await this.runCli([
|
|
1080
|
+
"sandbox",
|
|
1081
|
+
"create",
|
|
1082
|
+
"--help"
|
|
1083
|
+
], "openshell sandbox create capability check failed");
|
|
1106
1084
|
return /^\s*--detach(?:\s|$)/mu.test(result.stdout);
|
|
1107
1085
|
})();
|
|
1108
1086
|
openShellDetachedCreateSupport = {
|
|
@@ -1120,20 +1098,16 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1120
1098
|
async resolveLegacyRuntimePhase() {
|
|
1121
1099
|
const pageSize = 100;
|
|
1122
1100
|
for (let offset = 0;; offset += pageSize) {
|
|
1123
|
-
const listResult = await
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
"json"
|
|
1134
|
-
],
|
|
1135
|
-
cwd: this.params.createParams.workspaceDir
|
|
1136
|
-
});
|
|
1101
|
+
const listResult = await this.runCli([
|
|
1102
|
+
"sandbox",
|
|
1103
|
+
"list",
|
|
1104
|
+
"--limit",
|
|
1105
|
+
String(pageSize),
|
|
1106
|
+
"--offset",
|
|
1107
|
+
String(offset),
|
|
1108
|
+
"--output",
|
|
1109
|
+
"json"
|
|
1110
|
+
]);
|
|
1137
1111
|
if (listResult.code !== 0) throw this.buildLegacyRuntimeUnavailableError(listResult.stderr.trim());
|
|
1138
1112
|
const page = parseOpenShellSandboxPhasePage(listResult.stdout, this.params.execContext.sandboxName);
|
|
1139
1113
|
if (!page) throw this.buildLegacyRuntimeUnavailableError("OpenShell returned malformed sandbox lifecycle data.");
|
|
@@ -1203,18 +1177,13 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1203
1177
|
rootDir: resolveOpenShellTmpRoot(),
|
|
1204
1178
|
prefix: "openclaw-openshell-sync-"
|
|
1205
1179
|
}, async ({ dir: tmpDir }) => {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
tmpDir
|
|
1214
|
-
],
|
|
1215
|
-
cwd: this.params.createParams.workspaceDir
|
|
1216
|
-
});
|
|
1217
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox download failed");
|
|
1180
|
+
await this.runCli([
|
|
1181
|
+
"sandbox",
|
|
1182
|
+
"download",
|
|
1183
|
+
this.params.execContext.sandboxName,
|
|
1184
|
+
root.remote,
|
|
1185
|
+
tmpDir
|
|
1186
|
+
], "openshell sandbox download failed");
|
|
1218
1187
|
const preservedShadows = [];
|
|
1219
1188
|
const failures = [];
|
|
1220
1189
|
try {
|
|
@@ -1276,18 +1245,7 @@ var OpenShellSandboxBackendImpl = class {
|
|
|
1276
1245
|
targetDir: stagedRoot,
|
|
1277
1246
|
excludeDirs: DEFAULT_OPEN_SHELL_MIRROR_EXCLUDE_DIRS
|
|
1278
1247
|
});
|
|
1279
|
-
for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) {
|
|
1280
|
-
const result = await runOpenShellCli({
|
|
1281
|
-
context: this.params.execContext,
|
|
1282
|
-
args: buildOpenShellDirectoryUploadArgs({
|
|
1283
|
-
sandboxName: this.params.execContext.sandboxName,
|
|
1284
|
-
localPath: path.join(stagedRoot, entry),
|
|
1285
|
-
remotePath
|
|
1286
|
-
}),
|
|
1287
|
-
cwd: this.params.createParams.workspaceDir
|
|
1288
|
-
});
|
|
1289
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || "openshell sandbox upload failed");
|
|
1290
|
-
}
|
|
1248
|
+
for (const entry of (await fsPromises.readdir(stagedRoot)).toSorted()) await this.uploadFileToRemote(path.join(stagedRoot, entry), `${normalizeRemotePath(remotePath)}/`);
|
|
1291
1249
|
});
|
|
1292
1250
|
}
|
|
1293
1251
|
async remoteManagedRootsEmpty() {
|
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.25.0 - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- **Faster synchronous plugin captures:** 16% faster than pre-change main on a measured 6,000-file tree; the new `createRootFileCopyBatchSync` API on `advanced` also reuses directory admissions across copies. ([#878](https://github.com/openclaw/fs-safe/pull/878), [#849](https://github.com/openclaw/fs-safe/pull/849))
|
|
10
|
+
- **Guest workspace publication:** works on gVisor and other Linux filesystems that reject `RENAME_NOREPLACE`. ([#876](https://github.com/openclaw/fs-safe/pull/876))
|
|
11
|
+
- **Root-backed sidecar locks:** native asynchronous release and exit cleanup no longer strand locks after their Root moves. ([#879](https://github.com/openclaw/fs-safe/pull/879))
|
|
12
|
+
- **Guest cross-device moves:** preserve the admitted file permissions. ([#869](https://github.com/openclaw/fs-safe/pull/869))
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **Guarded copy batches:** `createRootFileCopyBatchSync` on `advanced` reuses directory admissions across synchronous copies on canonical POSIX paths while rechecking ancestor and file identities for every copy; single-file copies also reuse admission within the operation. ([#849](https://github.com/openclaw/fs-safe/pull/849))
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- **Root-backed file locks:** native asynchronous release and exit cleanup follow the created lock's retained identity after its Root moves, preserving replacement files and reporting ownership mismatches. ([#879](https://github.com/openclaw/fs-safe/pull/879)) Thanks @axiom-ncis.
|
|
21
|
+
- **Temp directory diagnostics:** name rejected roots and ancestors, explain ownership, mode, or symlink failures, and provide safe recovery guidance, including for root-run appliances. Thanks @joequant. ([#877](https://github.com/openclaw/fs-safe/pull/877))
|
|
22
|
+
- **Guest directory publication:** publish staged workspaces when Linux filesystems reject `RENAME_NOREPLACE`, with checked file link/unlink fallback and directory rename that can only replace a concurrently created empty directory. ([#876](https://github.com/openclaw/fs-safe/pull/876)) Thanks @valkyriweb.
|
|
23
|
+
- **Guest cross-device moves:** preserve the admitted source file's permissions instead of leaving the destination with a broader default mode. ([#869](https://github.com/openclaw/fs-safe/pull/869))
|
|
24
|
+
- **Move receipts:** publication receipts capture the source identity after synchronous authorization callbacks return, so a source replaced during authorization is reported accurately. ([#868](https://github.com/openclaw/fs-safe/pull/868))
|
|
25
|
+
- **Atomic copy fallback:** report a failed restoration as a failure even when the underlying error carries a restored receipt. ([#851](https://github.com/openclaw/fs-safe/pull/851))
|
|
26
|
+
- **Archive read limits:** keep the validated entry-read byte limit fixed when callers mutate the options during a read. ([#852](https://github.com/openclaw/fs-safe/pull/852))
|
|
27
|
+
- **Watch on macOS:** reconcile notifications for changes to the watched Root itself immediately instead of waiting for the periodic scan. ([#858](https://github.com/openclaw/fs-safe/pull/858))
|
|
28
|
+
- **`pathScope` tilde names:** keep literal `~` directory names inside scoped file and existing-path lookups instead of expanding them. ([#856](https://github.com/openclaw/fs-safe/pull/856))
|
|
29
|
+
|
|
30
|
+
### Performance
|
|
31
|
+
|
|
32
|
+
- **Guarded file copies:** reuse checked canonical POSIX roots during initial path admission, reducing repeated path-resolution work in synchronous plugin source captures without changing per-file identity or race checks.
|
|
33
|
+
- **Root walks:** reuse normalized directory prefixes, cutting a 50k-file walk by 5.6% and its sampled allocation by about 9% while keeping every filesystem and identity check. ([#870](https://github.com/openclaw/fs-safe/pull/870))
|
|
34
|
+
- **Atomic writes:** non-durable async POSIX writes skip a redundant mode change when the staged file already has the requested permissions. ([#871](https://github.com/openclaw/fs-safe/pull/871))
|
|
35
|
+
- **Guarded mutations:** fewer promise allocations around synchronous guard checks, with directory identity checks unchanged. ([#857](https://github.com/openclaw/fs-safe/pull/857))
|
|
36
|
+
- **Archive publication:** fewer promise allocations in archive guard checks, without removing identity or containment checks. ([#864](https://github.com/openclaw/fs-safe/pull/864))
|
|
37
|
+
- **Watch:** Linux and Windows watch passes skip macOS stream-path preparation. ([#860](https://github.com/openclaw/fs-safe/pull/860))
|
|
38
|
+
- **Permission diagnostics:** bound escaping work for large error messages without changing the displayed excerpts. ([#859](https://github.com/openclaw/fs-safe/pull/859))
|
|
39
|
+
|
|
5
40
|
## 0.24.2 - 2026-10-07
|
|
6
41
|
|
|
7
42
|
### Fixes
|
|
@@ -22,7 +22,7 @@ await fs.write("../escape.txt", "x"); // throws FsSafeError("outside-
|
|
|
22
22
|
|
|
23
23
|
That's the whole pitch. `root()` is the product; the rest of the package — JSON stores, atomic writes, secret files, archive extraction, temp workspaces — is supporting cast for the same boundary.
|
|
24
24
|
|
|
25
|
-
Full docs and reference at **[fs-safe.io](https://fs-safe.io)**.
|
|
25
|
+
Full docs and reference at **[fs-safe.io](https://fs-safe.io)**. For synchronous plugin captures, [guarded file copies and batches](docs/copy.md#synchronous-guarded-file-copies) return owned destination descriptors while preserving per-file checks.
|
|
26
26
|
|
|
27
27
|
## Contents
|
|
28
28
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { copyRootFileSync, type CopyRootFileSyncOptions, type CopiedRootFileSync } from "./copy-root-file-sync.js";
|
|
1
|
+
export { copyRootFileSync, createRootFileCopyBatchSync, type RootFileCopyBatchSync, type CopyRootFileSyncOptions, type CopiedRootFileSync } from "./copy-root-file-sync.js";
|
|
2
2
|
export { createAsyncLock } from "./async-lock.js";
|
|
3
3
|
export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
|
|
4
4
|
export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { copyRootFileSync } from "./copy-root-file-sync.js";
|
|
1
|
+
export { copyRootFileSync, createRootFileCopyBatchSync } from "./copy-root-file-sync.js";
|
|
2
2
|
// Advanced composition surface. These exports are less stable than the focused
|
|
3
3
|
// public subpaths; prefer root/json/store/temp/archive unless you are building a
|
|
4
4
|
// higher-level primitive.
|
|
@@ -65,7 +65,7 @@ export async function finalizeArchivePublication(params) {
|
|
|
65
65
|
const check = () => params.deadline?.check();
|
|
66
66
|
const assertGuards = async (guards) => {
|
|
67
67
|
for (const guard of [params.destinationGuard, ...guards, params.sourceGuard]) {
|
|
68
|
-
|
|
68
|
+
assertDirectoryIdentityGuard(guard);
|
|
69
69
|
check();
|
|
70
70
|
}
|
|
71
71
|
};
|
|
@@ -98,15 +98,15 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
|
|
|
98
98
|
assertSourceDirectory(ancestor);
|
|
99
99
|
};
|
|
100
100
|
const assertGuards = async () => {
|
|
101
|
-
|
|
101
|
+
assertDirectoryIdentityGuard(destinationGuard);
|
|
102
102
|
check();
|
|
103
103
|
for (const ancestor of ancestors) {
|
|
104
|
-
|
|
104
|
+
assertDirectoryIdentityGuard(ancestor.guard);
|
|
105
105
|
check();
|
|
106
106
|
await ancestor.owner.verify(check);
|
|
107
107
|
check();
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
assertDirectoryIdentityGuard(sourceGuard);
|
|
110
110
|
check();
|
|
111
111
|
};
|
|
112
112
|
const walk = async (sourceDir) => {
|
|
@@ -184,7 +184,7 @@ async function mergeTree(params, publication, durable = true, entryUmask = 0) {
|
|
|
184
184
|
await owner.apply(mode, { check, beforeChmod: async () => {
|
|
185
185
|
await assertGuards();
|
|
186
186
|
assertSourceAncestors();
|
|
187
|
-
|
|
187
|
+
assertDirectoryIdentityGuard(guard);
|
|
188
188
|
check();
|
|
189
189
|
await assertResolvedInsideDestination({
|
|
190
190
|
destinationRealDir, targetPath: destinationPath, originalPath,
|
|
Binary file
|
|
@@ -213,7 +213,8 @@ async function readNativeBufferEntry(native, buffer, kind, requested, displayPat
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
216
|
-
|
|
216
|
+
const maxBytes = options.maxBytes;
|
|
217
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
217
218
|
throw new RangeError("maxBytes must be a non-negative safe integer");
|
|
218
219
|
}
|
|
219
220
|
const kind = options.kind ?? resolveArchiveKind(archivePath);
|
|
@@ -228,7 +229,7 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
228
229
|
admitZipBuffer(buffer, resolveExtractLimits(), entry => { zipEntries.push(entry); });
|
|
229
230
|
const native = getNativeBinding();
|
|
230
231
|
if (native)
|
|
231
|
-
return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath,
|
|
232
|
-
return kind === "zip" ? await readZipEntry(buffer, requestedEntry,
|
|
233
|
-
: await readTarEntry(buffer, requestedEntry,
|
|
232
|
+
return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, maxBytes, zipEntries);
|
|
233
|
+
return kind === "zip" ? await readZipEntry(buffer, requestedEntry, maxBytes, zipEntries)
|
|
234
|
+
: await readTarEntry(buffer, requestedEntry, maxBytes, kind);
|
|
234
235
|
}
|
|
@@ -5,7 +5,7 @@ import { ArchiveSecurityError } from "./archive-errors.js";
|
|
|
5
5
|
export type ArchiveDirectoryGuard = AsyncDirectoryGuard<BigIntStats>;
|
|
6
6
|
export declare function createArchiveSymlinkTraversalError(originalPath: string): ArchiveSecurityError;
|
|
7
7
|
export declare function createDirectoryIdentityGuard(dir: string): Promise<ArchiveDirectoryGuard>;
|
|
8
|
-
export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard):
|
|
8
|
+
export declare function assertDirectoryIdentityGuard(guard: ArchiveDirectoryGuard): void;
|
|
9
9
|
export declare function prepareArchiveDestinationGuard(destDir: string): Promise<ArchiveDirectoryGuard>;
|
|
10
10
|
export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
|
|
11
11
|
export declare function assertResolvedInsideDestination(params: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fsSync, {} from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { assertDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
5
|
import { ownExtractionDestinationMutation, } from "./archive-deadline.js";
|
|
6
6
|
import { ArchiveSecurityError, } from "./archive-errors.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
@@ -38,9 +38,9 @@ export async function createDirectoryIdentityGuard(dir) {
|
|
|
38
38
|
throw err;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export
|
|
41
|
+
export function assertDirectoryIdentityGuard(guard) {
|
|
42
42
|
try {
|
|
43
|
-
|
|
43
|
+
assertDirectoryGuard(guard, "native");
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
46
46
|
if (err instanceof FsSafeError || isNotFoundPathError(err)) {
|
|
@@ -200,7 +200,7 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
|
|
|
200
200
|
if (existingDestinationGuard && assertGuards)
|
|
201
201
|
await assertGuards();
|
|
202
202
|
else {
|
|
203
|
-
|
|
203
|
+
assertDirectoryIdentityGuard(destinationGuard);
|
|
204
204
|
checkExtractionDeadline(params.deadline);
|
|
205
205
|
await assertGuards?.();
|
|
206
206
|
}
|
|
@@ -213,46 +213,30 @@ async function prepareOutputPath(params, assertGuards, privateWorkingMode = fals
|
|
|
213
213
|
originalPath: params.originalPath,
|
|
214
214
|
});
|
|
215
215
|
checkExtractionDeadline(params.deadline);
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
const relativePath = params.isDirectory ? relPath : path.posix.dirname(relPath);
|
|
217
|
+
if (params.isDirectory || relativePath !== ".") {
|
|
218
|
+
const directoryPath = params.isDirectory ? params.outPath : path.dirname(params.outPath);
|
|
219
|
+
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", directoryPath);
|
|
218
220
|
checkExtractionDeadline(params.deadline);
|
|
219
221
|
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
220
222
|
await assertOutputGuards();
|
|
221
223
|
checkExtractionDeadline(params.deadline);
|
|
222
|
-
await mkdirArchiveOutput({
|
|
223
|
-
targetRoot,
|
|
224
|
-
relativePath: relPath,
|
|
225
|
-
originalPath: params.originalPath,
|
|
226
|
-
});
|
|
227
|
-
checkExtractionDeadline(params.deadline);
|
|
228
|
-
await assertDirectoryIdentityGuard(destinationGuard);
|
|
224
|
+
await mkdirArchiveOutput({ targetRoot, relativePath, originalPath: params.originalPath });
|
|
229
225
|
checkExtractionDeadline(params.deadline);
|
|
230
|
-
|
|
231
|
-
destinationRealDir: params.destinationRealDir,
|
|
232
|
-
targetPath: params.outPath,
|
|
233
|
-
originalPath: params.originalPath,
|
|
234
|
-
});
|
|
235
|
-
checkExtractionDeadline(params.deadline);
|
|
236
|
-
});
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const parentRel = path.posix.dirname(relPath);
|
|
240
|
-
if (parentRel !== ".") {
|
|
241
|
-
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
|
|
242
|
-
checkExtractionDeadline(params.deadline);
|
|
243
|
-
await ownExtractionDestinationMutation(params.deadline, async () => {
|
|
244
|
-
await assertOutputGuards();
|
|
245
|
-
checkExtractionDeadline(params.deadline);
|
|
246
|
-
await mkdirArchiveOutput({
|
|
247
|
-
targetRoot,
|
|
248
|
-
relativePath: parentRel,
|
|
249
|
-
originalPath: params.originalPath,
|
|
250
|
-
});
|
|
251
|
-
checkExtractionDeadline(params.deadline);
|
|
252
|
-
await assertDirectoryIdentityGuard(destinationGuard);
|
|
226
|
+
assertDirectoryIdentityGuard(destinationGuard);
|
|
253
227
|
checkExtractionDeadline(params.deadline);
|
|
228
|
+
if (params.isDirectory) {
|
|
229
|
+
await assertResolvedInsideDestination({
|
|
230
|
+
destinationRealDir: params.destinationRealDir,
|
|
231
|
+
targetPath: params.outPath,
|
|
232
|
+
originalPath: params.originalPath,
|
|
233
|
+
});
|
|
234
|
+
checkExtractionDeadline(params.deadline);
|
|
235
|
+
}
|
|
254
236
|
});
|
|
255
237
|
}
|
|
238
|
+
if (params.isDirectory)
|
|
239
|
+
return;
|
|
256
240
|
await assertResolvedInsideDestination({
|
|
257
241
|
destinationRealDir: params.destinationRealDir,
|
|
258
242
|
targetPath: path.dirname(params.outPath),
|
|
@@ -292,12 +276,12 @@ export async function withStagedArchiveDestination(params) {
|
|
|
292
276
|
const stagingGuard = await createDirectoryIdentityGuard(stagingDir);
|
|
293
277
|
try {
|
|
294
278
|
await fs.chmod(stagingDir, ARCHIVE_STAGING_MODE).catch(() => undefined);
|
|
295
|
-
|
|
279
|
+
assertDirectoryIdentityGuard(stagingGuard);
|
|
296
280
|
return await params.run(stagingDir);
|
|
297
281
|
}
|
|
298
282
|
finally {
|
|
299
283
|
try {
|
|
300
|
-
|
|
284
|
+
assertDirectoryIdentityGuard(stagingGuard);
|
|
301
285
|
await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
302
286
|
}
|
|
303
287
|
catch {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
|
+
export type CopyPath = {
|
|
3
|
+
rootPath: string;
|
|
4
|
+
absolutePath: string;
|
|
5
|
+
};
|
|
6
|
+
declare function admitUncachedCopyPath(input: CopyPath, create?: boolean, admittedRoot?: AnyAsyncDirectoryGuard): {
|
|
7
|
+
path: string;
|
|
8
|
+
parent: AnyAsyncDirectoryGuard;
|
|
9
|
+
assertCurrent: () => void;
|
|
10
|
+
rootCanonicalPath: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Metadata custody lasts only for the owning copy or explicit batch. */
|
|
13
|
+
export declare function createCopyPathAdmissionCache(): {
|
|
14
|
+
admit: (input: CopyPath, create?: boolean) => ReturnType<typeof admitUncachedCopyPath>;
|
|
15
|
+
clear(): void;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertSyncDirectoryGuard, captureDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
7
|
+
import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
8
|
+
function admitUncachedCopyPath(input, create = false, admittedRoot) {
|
|
9
|
+
if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
|
|
10
|
+
throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
|
|
11
|
+
}
|
|
12
|
+
const root = admittedRoot ?? captureDirectoryGuard(realpathSync(input.rootPath), "native", { bigint: true });
|
|
13
|
+
// Canonical POSIX callers already own a freshly checked root. Keep the
|
|
14
|
+
// resolver's lexical/symlink walk, but do not rediscover that root each time.
|
|
15
|
+
const rootCanonicalPath = admittedRoot?.realPath;
|
|
16
|
+
const resolve = () => {
|
|
17
|
+
const parent = create ? resolveRootPathSync({
|
|
18
|
+
...input, rootCanonicalPath, absolutePath: path.dirname(input.absolutePath), boundaryLabel: "copy parent",
|
|
19
|
+
rejectSymlinks: true, rejectFinalSymlink: true,
|
|
20
|
+
}) : undefined;
|
|
21
|
+
const selected = resolveRootPathSync({
|
|
22
|
+
...input, rootCanonicalPath, boundaryLabel: "copy", rejectSymlinks: !create, rejectFinalSymlink: !create,
|
|
23
|
+
policy: create ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
24
|
+
});
|
|
25
|
+
if (parent && path.dirname(selected.canonicalPath) !== parent.canonicalPath) {
|
|
26
|
+
throw new FsSafeError("path-mismatch", "copy destination parent changed");
|
|
27
|
+
}
|
|
28
|
+
return selected;
|
|
29
|
+
};
|
|
30
|
+
const selected = resolve();
|
|
31
|
+
const parent = captureDirectoryGuard(path.dirname(selected.canonicalPath), "native", { bigint: true });
|
|
32
|
+
const assertCurrent = () => {
|
|
33
|
+
assertSyncDirectoryGuard(root);
|
|
34
|
+
assertSyncDirectoryGuard(parent);
|
|
35
|
+
const current = resolve();
|
|
36
|
+
if (current.rootCanonicalPath !== root.realPath || current.canonicalPath !== selected.canonicalPath) {
|
|
37
|
+
throw new FsSafeError("path-mismatch", "copy path changed during operation");
|
|
38
|
+
}
|
|
39
|
+
assertSyncDirectoryGuard(root);
|
|
40
|
+
assertSyncDirectoryGuard(parent);
|
|
41
|
+
};
|
|
42
|
+
assertCurrent();
|
|
43
|
+
return { path: selected.canonicalPath, parent, assertCurrent, rootCanonicalPath: undefined };
|
|
44
|
+
}
|
|
45
|
+
/** Metadata custody lasts only for the owning copy or explicit batch. */
|
|
46
|
+
export function createCopyPathAdmissionCache() {
|
|
47
|
+
const roots = new Map();
|
|
48
|
+
const parents = new Map();
|
|
49
|
+
const directories = new Map();
|
|
50
|
+
const assertParent = ({ root, parent, chain }) => {
|
|
51
|
+
assertSyncDirectoryGuard(root);
|
|
52
|
+
for (const guard of chain) {
|
|
53
|
+
inspectDirectoryIdentitySync(guard.dir, { dev: BigInt(guard.stat.dev), ino: BigInt(guard.stat.ino) });
|
|
54
|
+
}
|
|
55
|
+
assertSyncDirectoryGuard(parent);
|
|
56
|
+
assertSyncDirectoryGuard(root);
|
|
57
|
+
};
|
|
58
|
+
const admit = (input, create = false) => {
|
|
59
|
+
// Keep the public resolver for Windows aliases and noncanonical spellings,
|
|
60
|
+
// including raw dot segments whose traversal must not be normalized away.
|
|
61
|
+
if (process.platform === "win32" || !path.isAbsolute(input.rootPath) ||
|
|
62
|
+
!path.isAbsolute(input.absolutePath) || input.rootPath.includes("\0") || input.absolutePath.includes("\0") ||
|
|
63
|
+
path.resolve(input.rootPath) !== input.rootPath || path.resolve(input.absolutePath) !== input.absolutePath ||
|
|
64
|
+
!input.absolutePath.startsWith(input.rootPath.endsWith(path.sep) ? input.rootPath : `${input.rootPath}${path.sep}`)) {
|
|
65
|
+
return admitUncachedCopyPath(input, create);
|
|
66
|
+
}
|
|
67
|
+
let root = roots.get(input.rootPath);
|
|
68
|
+
if (!root) {
|
|
69
|
+
const canonical = realpathSync(input.rootPath);
|
|
70
|
+
if (canonical !== input.rootPath)
|
|
71
|
+
return admitUncachedCopyPath(input, create);
|
|
72
|
+
root = captureDirectoryGuard(canonical, "native", { bigint: true });
|
|
73
|
+
roots.set(input.rootPath, root);
|
|
74
|
+
}
|
|
75
|
+
assertSyncDirectoryGuard(root);
|
|
76
|
+
const parentPath = path.dirname(input.absolutePath);
|
|
77
|
+
let byParent = parents.get(input.rootPath);
|
|
78
|
+
let admitted = byParent?.get(parentPath);
|
|
79
|
+
if (!admitted) {
|
|
80
|
+
// The first file in each parent retains the existing lexical admission.
|
|
81
|
+
const selected = admitUncachedCopyPath(input, create, root);
|
|
82
|
+
const chain = [];
|
|
83
|
+
let cursor = input.rootPath;
|
|
84
|
+
const relative = path.relative(input.rootPath, parentPath);
|
|
85
|
+
for (const segment of relative ? relative.split(path.sep) : []) {
|
|
86
|
+
cursor = path.join(cursor, segment);
|
|
87
|
+
let guard = directories.get(cursor);
|
|
88
|
+
if (!guard) {
|
|
89
|
+
guard = captureDirectoryGuard(cursor, "native", { bigint: true });
|
|
90
|
+
directories.set(cursor, guard);
|
|
91
|
+
}
|
|
92
|
+
chain.push(guard);
|
|
93
|
+
}
|
|
94
|
+
admitted = { root, parent: chain.at(-1) ?? root, chain };
|
|
95
|
+
assertParent(admitted);
|
|
96
|
+
if (selected.path !== input.absolutePath || selected.parent.realPath !== parentPath ||
|
|
97
|
+
!sameFileIdentity(selected.parent.stat, admitted.parent.stat)) {
|
|
98
|
+
throw new FsSafeError("path-mismatch", "copy path changed during admission");
|
|
99
|
+
}
|
|
100
|
+
byParent ??= new Map();
|
|
101
|
+
byParent.set(parentPath, admitted);
|
|
102
|
+
parents.set(input.rootPath, byParent);
|
|
103
|
+
}
|
|
104
|
+
const selected = admitted;
|
|
105
|
+
const assertCurrent = () => {
|
|
106
|
+
assertParent(selected);
|
|
107
|
+
// No file observation is cached. Preserve the resolver's symlink errors.
|
|
108
|
+
if (!create && fs.lstatSync(input.absolutePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
109
|
+
admitUncachedCopyPath(input, create);
|
|
110
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
assertCurrent();
|
|
114
|
+
return { path: input.absolutePath, parent: selected.parent, assertCurrent, rootCanonicalPath: selected.root.realPath };
|
|
115
|
+
};
|
|
116
|
+
return { admit, clear() { roots.clear(); parents.clear(); directories.clear(); } };
|
|
117
|
+
}
|