@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.1
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 +91 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- 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 +38 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- 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/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -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-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +52 -6
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- 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-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- 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/sibling-staged-file.js +18 -3
- 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/staged-file-settlement.js +4 -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/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- 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 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
- 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 +109 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +21 -4
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +16 -16
- 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/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,97 @@
|
|
|
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
|
+
|
|
40
|
+
## 0.24.2 - 2026-10-07
|
|
41
|
+
|
|
42
|
+
### Fixes
|
|
43
|
+
|
|
44
|
+
- **Linux exclusive publication:** `publishFileExclusive` with `rename-noreplace` uses the identity-checked link/unlink move fallback in native `auto` when `RENAME_NOREPLACE` is unsupported, consumes the source on success, and reports `fallback: "link-unlink"` plus precise partial-failure receipts; native `require` stays fail-closed. Link/unlink is not crash-atomic. ([#843](https://github.com/openclaw/fs-safe/pull/843), [openclaw#166246](https://github.com/openclaw/openclaw/issues/166246)) Thanks @MertBasar0.
|
|
45
|
+
- **Native clone descriptors:** native file copies return read-write descriptors, so Linux clones can be read immediately and macOS clones can be written while preserving read-only source modes. ([#844](https://github.com/openclaw/fs-safe/pull/844))
|
|
46
|
+
|
|
47
|
+
## 0.24.1 - 2026-10-06
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
- **Pinned source copies:** `copyRootFileSync` accepts `expectedSourceIdentity` to reject a replaced source before destination creation and returns the verified `sourceIdentity` for every copy method. This refines the `copyRootFileSync` API introduced in 0.24.0. ([#841](https://github.com/openclaw/fs-safe/pull/841))
|
|
52
|
+
|
|
53
|
+
## 0.24.0 - 2026-10-06
|
|
54
|
+
|
|
55
|
+
### Highlights
|
|
56
|
+
|
|
57
|
+
- **No-replace moves on more Linux filesystems:** when filesystems reject `renameat2` with `RENAME_NOREPLACE`, including reported QNAP ZFS shares and some overlay mounts, `auto` falls back safely: directories use plain rename that can at most replace an empty directory, and files use identity-checked link/unlink that cannot overwrite a target. Native `require` fails closed. ([#838](https://github.com/openclaw/fs-safe/pull/838), fixes [openclaw#165617](https://github.com/openclaw/openclaw/issues/165617)) Thanks @StarGazer1995, original reporter @bbs2i58, and @MertBasar0.
|
|
58
|
+
- **Synchronous guarded copies:** new `copyRootFileSync` on `advanced` supports APFS cloning, reflinks, `copy_file_range`, and guarded byte fallback, returning an owned destination descriptor for synchronous capture hashes. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
59
|
+
- **Public capability classification:** new `isNoReplaceUnsupported(error)` identifies unavailable no-replace rename support without matching internal native error codes. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
60
|
+
|
|
61
|
+
### Features
|
|
62
|
+
|
|
63
|
+
- **Synchronous root-to-root copy:** `copyRootFileSync` exclusively creates an absent destination under its admitted root, bounds the copy with `maxBytes`, verifies source and destination identities, and returns a disposable descriptor with copy-method and byte-count receipts. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
64
|
+
- **No-replace error classifier:** export `isNoReplaceUnsupported(error)` from the root package and `errors` subpath for stable `helper-unavailable` capability diagnostics. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
65
|
+
|
|
66
|
+
### Fixes
|
|
67
|
+
|
|
68
|
+
- **Linux no-replace moves:** `Root.move()` uses retained-parent file link/unlink or directory rename when `RENAME_NOREPLACE` is unsupported in `auto`, rejects file collisions, and preserves publication/source-removal details after partial failures. ([#838](https://github.com/openclaw/fs-safe/pull/838)) Thanks @StarGazer1995, @bbs2i58, and @MertBasar0.
|
|
69
|
+
|
|
70
|
+
### Performance
|
|
71
|
+
|
|
72
|
+
- **Synchronous copy acceleration:** reuse the native bounded copy engine for APFS clones, Linux reflinks, and `copy_file_range` before byte copying when `copyRootFileSync` is called with `clone: "auto"`. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
73
|
+
|
|
74
|
+
### Compatibility
|
|
75
|
+
|
|
76
|
+
- **Move fallback limits:** native `require` keeps refusing unsupported no-replace moves. In `auto`, the directory fallback can replace a concurrently created empty directory; nonempty directories and non-directory competitors reject. File fallback requires hard-link support and fails closed if links are denied or unsupported. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
77
|
+
- **Synchronous copy contract:** `copyRootFileSync` defaults to `clone: "never"`, requires existing parents and an absent destination, and performs direct exclusive creation. Use a private capture directory; it does not promise atomic publication, crash durability, or a coherent snapshot of concurrent in-place edits. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
78
|
+
|
|
79
|
+
## 0.23.1 - 2026-10-04
|
|
80
|
+
|
|
81
|
+
### Highlights
|
|
82
|
+
|
|
83
|
+
- **gVisor and 9p file creation:** `create()` and other no-replace publication fall back in `auto` mode on filesystems that reject `renameat2` with `RENAME_NOREPLACE`; `require` mode fails closed. ([#834](https://github.com/openclaw/fs-safe/pull/834), fixes [#828](https://github.com/openclaw/fs-safe/issues/828)) Thanks @vishnukool.
|
|
84
|
+
- **Container copy compatibility:** seccomp or LSM policies denying `FICLONE` no longer break `clone: "auto"` copies, for both single files and `copyTree`. ([#829](https://github.com/openclaw/fs-safe/pull/829), [#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @Mohl, with co-author credit to @foxsky.
|
|
85
|
+
- **macOS watching:** a FIFO at a watched name no longer freezes watching across the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
|
|
86
|
+
|
|
87
|
+
### Fixes
|
|
88
|
+
|
|
89
|
+
- **gVisor file creation:** fall back safely when the filesystem rejects native no-replace publication, clean unpublished stages, and report the missing capability in native-required mode. ([#834](https://github.com/openclaw/fs-safe/pull/834)) Thanks @vishnukool.
|
|
90
|
+
- **ZIP collision checks:** reject directory and file names that differ only by case, Unicode normalization, or a trailing separator before extraction or bounded reads. ([#832](https://github.com/openclaw/fs-safe/pull/832)) Thanks @SebTardif.
|
|
91
|
+
- **macOS entry watch:** prevent a FIFO at a watched name from blocking every watcher in the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
|
|
92
|
+
- **Disk-full diagnostics:** preserve disk-full (`ENOSPC`) diagnostics and cleanup receipts through combined write and disposal failures instead of reporting a misleading file-type error. ([#833](https://github.com/openclaw/fs-safe/pull/833)) Thanks @steipete.
|
|
93
|
+
- **Linux clone fallback:** copies with `clone: "auto"` fall back to guarded byte copying when container seccomp or LSM policy denies `FICLONE`; `clone: "always"` reports `unsupported-platform` and removes its stage. ([#829](https://github.com/openclaw/fs-safe/pull/829)) Thanks @Mohl, with co-author credit to @foxsky.
|
|
94
|
+
- **Linux tree clone fallback:** `copyTree` with `clone: "auto"` copies bytes when XFS/ZFS file reflinks are unavailable or denied by container policy; `"always"` reports `unsupported-platform` after partial-tree cleanup. ([#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @steipete.
|
|
95
|
+
|
|
5
96
|
## 0.23.0 - 2026-10-02
|
|
6
97
|
|
|
7
98
|
### Highlights
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
export { copyRootFileSync, createRootFileCopyBatchSync, type RootFileCopyBatchSync, type CopyRootFileSyncOptions, type CopiedRootFileSync } from "./copy-root-file-sync.js";
|
|
1
2
|
export { createAsyncLock } from "./async-lock.js";
|
|
2
3
|
export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
|
|
3
4
|
export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { copyRootFileSync, createRootFileCopyBatchSync } from "./copy-root-file-sync.js";
|
|
1
2
|
// Advanced composition surface. These exports are less stable than the focused
|
|
2
3
|
// public subpaths; prefer root/json/store/temp/archive unless you are building a
|
|
3
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
|
};
|
|
@@ -63,13 +63,17 @@ export function stripArchivePath(entryPath, stripComponents) {
|
|
|
63
63
|
export function createArchiveOutputPathTracker() {
|
|
64
64
|
const seen = new Set();
|
|
65
65
|
return (entryPath, originalPath) => {
|
|
66
|
-
|
|
66
|
+
// Extraction passes stripArchivePath output. Native manifests and physical
|
|
67
|
+
// admission pass raw names, and path.posix.normalize keeps a directory's
|
|
68
|
+
// trailing slash, so "A/" and "a" would not share a key.
|
|
69
|
+
const canonical = stripArchivePath(entryPath, 0)
|
|
70
|
+
?? path.posix.normalize(normalizeArchiveEntryPath(entryPath));
|
|
67
71
|
// Archive policy must not depend on the destination volume's case or
|
|
68
72
|
// Unicode-normalization behavior. Otherwise the JavaScript and native
|
|
69
73
|
// writers can disagree about which of two colliding entries wins.
|
|
70
|
-
const collisionKey = lowerCaseNfc(
|
|
74
|
+
const collisionKey = lowerCaseNfc(canonical);
|
|
71
75
|
if (seen.has(collisionKey)) {
|
|
72
|
-
throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(
|
|
76
|
+
throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(canonical)}: ${formatErrorDetail(originalPath)}`);
|
|
73
77
|
}
|
|
74
78
|
seen.add(collisionKey);
|
|
75
79
|
};
|
|
@@ -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 {};
|